diff --git a/.gitignore b/.gitignore index c9bae27f3..9167f8be3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,304 +1,305 @@ # Created by https://www.gitignore.io/api/visualstudio ### VisualStudio ### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo *.user *.userosscache *.sln.docstates *.vcxproj.filters # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # NuGET [Nn]uget/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json project.fragment.lock.json artifacts/ **/Properties/launchSettings.json *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/ ### VisualStudio Patch ### build/ ### Built docs -docs/_site/ +DisCatSharp.Docs/_site/ +DisCatSharp.Docs/_site_pdf/ # Docs build artifacts docfx/ 7zip/ # Build artifacts artifacts/ ### Test files config.json ffmpeg.exe *.pcm appveyor-test.ps1 *.diff CodeMap1.dgml .vscode/launch.json diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 31edf8feb..b89e54533 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,1352 +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 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 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. /// 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?.Services, + 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?.Services, + 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/AutocompleteContext.cs b/DisCatSharp.ApplicationCommands/Context/AutocompleteContext.cs index 13457508b..20d08fb91 100644 --- a/DisCatSharp.ApplicationCommands/Context/AutocompleteContext.cs +++ b/DisCatSharp.ApplicationCommands/Context/AutocompleteContext.cs @@ -1,48 +1,88 @@ // 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 DisCatSharp.Entities; +using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.ApplicationCommands { /// /// Represents a context for an autocomplete interaction. /// public class AutocompleteContext { /// /// The interaction 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 slash command module this interaction was created in. + /// + public ApplicationCommandsExtension ApplicationCommandsExtension { 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); + /// /// The options already provided. /// public IReadOnlyList Options { get; internal set; } /// /// The option to autocomplete. /// public DiscordInteractionDataOption FocusedOption { get; internal set; } } } diff --git a/DisCatSharp.ApplicationCommands/EventArgs/ContextMenu/ContextMenuErrorEventArgs.cs b/DisCatSharp.ApplicationCommands/EventArgs/ContextMenu/ContextMenuErrorEventArgs.cs index 2fbbc238a..c1baae975 100644 --- a/DisCatSharp.ApplicationCommands/EventArgs/ContextMenu/ContextMenuErrorEventArgs.cs +++ b/DisCatSharp.ApplicationCommands/EventArgs/ContextMenu/ContextMenuErrorEventArgs.cs @@ -1,48 +1,52 @@ // 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.Common.Utilities; using System; using DisCatSharp.EventArgs; namespace DisCatSharp.ApplicationCommands.EventArgs { /// /// Represents arguments for a /// public class ContextMenuErrorEventArgs : DiscordEventArgs { /// /// The context of the command. /// public ContextMenuContext Context { get; internal set; } /// /// The exception thrown. /// public Exception Exception { get; internal set; } + /// + /// Initializes a new instance of the class. + /// + /// The provider. public ContextMenuErrorEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp.ApplicationCommands/EventArgs/ContextMenu/ContextMenuExecutedEventArgs.cs b/DisCatSharp.ApplicationCommands/EventArgs/ContextMenu/ContextMenuExecutedEventArgs.cs index 2f756a143..badea6f28 100644 --- a/DisCatSharp.ApplicationCommands/EventArgs/ContextMenu/ContextMenuExecutedEventArgs.cs +++ b/DisCatSharp.ApplicationCommands/EventArgs/ContextMenu/ContextMenuExecutedEventArgs.cs @@ -1,42 +1,46 @@ // 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.Common.Utilities; using DisCatSharp.EventArgs; namespace DisCatSharp.ApplicationCommands.EventArgs { /// /// Represents the arguments for a event /// public sealed class ContextMenuExecutedEventArgs : DiscordEventArgs { /// /// The context of the command. /// public ContextMenuContext Context { get; internal set; } + /// + /// Initializes a new instance of the class. + /// + /// The provider. public ContextMenuExecutedEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp.ApplicationCommands/EventArgs/SlashCommand/SlashCommandErrorEventArgs.cs b/DisCatSharp.ApplicationCommands/EventArgs/SlashCommand/SlashCommandErrorEventArgs.cs index ba7abb0d6..10e86999c 100644 --- a/DisCatSharp.ApplicationCommands/EventArgs/SlashCommand/SlashCommandErrorEventArgs.cs +++ b/DisCatSharp.ApplicationCommands/EventArgs/SlashCommand/SlashCommandErrorEventArgs.cs @@ -1,47 +1,51 @@ // 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.Common.Utilities; using System; using DisCatSharp.EventArgs; namespace DisCatSharp.ApplicationCommands.EventArgs { /// /// Represents arguments for a event /// public class SlashCommandErrorEventArgs : DiscordEventArgs { /// /// The context of the command. /// public InteractionContext Context { get; internal set; } /// /// The exception thrown. /// public Exception Exception { get; internal set; } + /// + /// Initializes a new instance of the class. + /// + /// The provider. public SlashCommandErrorEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp.ApplicationCommands/EventArgs/SlashCommand/SlashCommandExecutedEventArgs.cs b/DisCatSharp.ApplicationCommands/EventArgs/SlashCommand/SlashCommandExecutedEventArgs.cs index 80b2f5aee..25c76260b 100644 --- a/DisCatSharp.ApplicationCommands/EventArgs/SlashCommand/SlashCommandExecutedEventArgs.cs +++ b/DisCatSharp.ApplicationCommands/EventArgs/SlashCommand/SlashCommandExecutedEventArgs.cs @@ -1,42 +1,46 @@ // 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.Common.Utilities; using DisCatSharp.EventArgs; namespace DisCatSharp.ApplicationCommands.EventArgs { /// /// Represents the arguments for a event /// public class SlashCommandExecutedEventArgs : DiscordEventArgs { /// /// The context of the command. /// public InteractionContext Context { get; internal set; } + /// + /// Initializes a new instance of the class. + /// + /// The provider. public SlashCommandExecutedEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp.CommandsNext/EventArgs/CommandErrorEventArgs.cs b/DisCatSharp.CommandsNext/EventArgs/CommandErrorEventArgs.cs index c05c96867..e6f99d7c7 100644 --- a/DisCatSharp.CommandsNext/EventArgs/CommandErrorEventArgs.cs +++ b/DisCatSharp.CommandsNext/EventArgs/CommandErrorEventArgs.cs @@ -1,40 +1,44 @@ // 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.CommandsNext { /// /// Represents arguments for event. /// public class CommandErrorEventArgs : CommandEventArgs { /// /// Gets the exception. /// public Exception Exception { get; internal set; } + /// + /// Initializes a new instance of the class. + /// + /// The provider. public CommandErrorEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp.CommandsNext/EventArgs/CommandEventArgs.cs b/DisCatSharp.CommandsNext/EventArgs/CommandEventArgs.cs index 528678582..13b1150a5 100644 --- a/DisCatSharp.CommandsNext/EventArgs/CommandEventArgs.cs +++ b/DisCatSharp.CommandsNext/EventArgs/CommandEventArgs.cs @@ -1,48 +1,52 @@ // 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.Common.Utilities; using DisCatSharp.EventArgs; namespace DisCatSharp.CommandsNext { /// /// Base class for all CNext-related events. /// public class CommandEventArgs : DiscordEventArgs { /// /// Gets the context in which the command was executed. /// public CommandContext Context { get; internal set; } /// /// Gets the command that was executed. /// public Command Command => this.Context.Command; + /// + /// Initializes a new instance of the class. + /// + /// The provider. public CommandEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp.CommandsNext/EventArgs/CommandExecutionEventArgs.cs b/DisCatSharp.CommandsNext/EventArgs/CommandExecutionEventArgs.cs index ebd2246a3..bd6ed3455 100644 --- a/DisCatSharp.CommandsNext/EventArgs/CommandExecutionEventArgs.cs +++ b/DisCatSharp.CommandsNext/EventArgs/CommandExecutionEventArgs.cs @@ -1,35 +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; namespace DisCatSharp.CommandsNext { /// /// Represents arguments for event. /// public class CommandExecutionEventArgs : CommandEventArgs { + /// + /// Initializes a new instance of the class. + /// + /// The provider. public CommandExecutionEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp.Docs/dcs/ManagedReference.extension.js b/DisCatSharp.Docs/dcs/ManagedReference.extension.js index e75cec347..10a4697ca 100644 --- a/DisCatSharp.Docs/dcs/ManagedReference.extension.js +++ b/DisCatSharp.Docs/dcs/ManagedReference.extension.js @@ -1,97 +1,97 @@ // Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. var Reset = "\x1b[0m" var Bright = "\x1b[1m" var Dim = "\x1b[2m" var Underscore = "\x1b[4m" var Blink = "\x1b[5m" var Reverse = "\x1b[7m" var Hidden = "\x1b[8m" var FgBlack = "\x1b[30m" var FgRed = "\x1b[31m" var FgGreen = "\x1b[32m" var FgYellow = "\x1b[33m" var FgBlue = "\x1b[34m" var FgMagenta = "\x1b[35m" var FgCyan = "\x1b[36m" var FgWhite = "\x1b[37m" var BgBlack = "\x1b[40m" var BgRed = "\x1b[41m" var BgGreen = "\x1b[42m" var BgYellow = "\x1b[43m" var BgBlue = "\x1b[44m" var BgMagenta = "\x1b[45m" var BgCyan = "\x1b[46m" var BgWhite = "\x1b[47m" /** * This method will be called at the start of exports.transform in ManagedReference.html.primary.js */ exports.preTransform = function (model) { return model; } /** * This method will be called at the end of exports.transform in ManagedReference.html.primary.js */ exports.postTransform = function (model) { try { //console.log('hi model: ' + model.type); - + model.aliases = []; //handleItem(model, model.aliases); // type if (model.children) { model.children.forEach(function(item) { // "id": "methods" or others //console.log('hi container: ' + item.typePropertyName) if (item.children) { item.children.forEach(function(child) { // actual method (or other member) //console.log('hi member: ' + child.id) handleItem(child, model.aliases); }); } }); } - + model.hasAliases = model.aliases.length > 0; - + } catch (e) { console.log(Bright + FgRed + '\nFail: ' + e + ',' + Object.keys(e) + Reset); } return model; } function handleItem(item, aliases) { //console.log('item: ' + JSON.stringify(item)); if (item.remarks) { - console.log('Remarks: ' + item.remarks); + //console.log('Remarks: ' + item.remarks); var itemAliases = []; item.remarks = item.remarks.replace(/\[alias=(['"]|")(.*?)\1]/g, function($$, $quot, $aliasName) { - console.log($$); - console.log($quot); - console.log($aliasName); + //console.log($$); + //console.log($quot); + //console.log($aliasName); var alias = { isAlias: true, name: item.name[0].value.replace(/^.*?(\(|$)/, $aliasName + '$1'), id: $aliasName, aliasTo: item.uid, - + targetXref: item.specName[0].value //targetName: item.name[0].value, //targetFullName: item.fullName[0].value } itemAliases.push(alias.id); aliases.push(alias); return ''; }); //item.itemHasAliases = item.aliases.length > 0; item.aliasesString = itemAliases.join(', '); itemAliases = null; - + item.remarks = item.remarks.trim().replace(/^

\s*<\/p>$/, ''); if (item.remarks.length == 0) item.remarks = null; //console.log('item: ' + JSON.stringify(item)); } } diff --git a/DisCatSharp.Docs/docfx-pdf.json b/DisCatSharp.Docs/docfx-pdf.json new file mode 100644 index 000000000..3f58d157b --- /dev/null +++ b/DisCatSharp.Docs/docfx-pdf.json @@ -0,0 +1,66 @@ +{ +"pdf": { + "content": [ + { + "files": [ + "api/**.yml", + "api/index.md" + ], + "exclude": [ + "**/toc.yml", + "**/toc.md" + ] + }, + { + "files": [ + "articles/**.md", + "articles/toc.yml", + "articles/**/toc.yml", + "natives/**.md", + "faq/**.md", + "toc.yml", + "*.md", + "pdf/*" + ], + "exclude": [ + "**/bin/**", + "**/obj/**", + "_site_pdf/**", + "**/toc.yml", + "**/toc.md" + ] + }, + { + "files": "pdf/toc.yml" + } + ], + "resource": [ + { + "files": [ + "images/**" + ], + "exclude": [ + "**/bin/**", + "**/obj/**", + "_site_pdf/**" + ] + } + ], + "overwrite": [ + { + "files": [ + "apidoc/**.md" + ], + "exclude": [ + "**/bin/**", + "**/obj/**", + "_site_pdf/**" + ] + } + ], + "wkhtmltopdf": { + "additionalArguments": "--enable-local-file-access" + }, + "dest": "_site_pdf" + } +} diff --git a/DisCatSharp.Docs/docfx.json b/DisCatSharp.Docs/docfx.json index 30a40d32c..301ec4fee 100644 --- a/DisCatSharp.Docs/docfx.json +++ b/DisCatSharp.Docs/docfx.json @@ -1,83 +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" + "articles/**.md", + "articles/**/toc.yml", + "natives/**.md", + "faq/**.md", + "toc.yml", + "*.md" ], "exclude": [ - "obj/**", - "_site/**" + "**/bin/**", + "**/obj/**", + "_site/**", + "_site_pdf/**" ] } ], "resource": [ { "files": [ "images/**", "natives/**.zip" ], "exclude": [ - "obj/**", - "_site/**" + "**/bin/**", + "**/obj/**", + "_site/**", + "_site_pdf/**" ] } ], "overwrite": [ { "files": [ - "apidoc/**.md" ], "exclude": [ - "obj/**", - "_site/**" + "**/bin/**", + "**/obj/**", + "_site/**", + "_site_pdf/**" ] } ], "dest": "_site", "globalMetadata": { "_appFooter": "© 2021 Aiko IT Systems", - "_enableSearch": "true" + "_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/pdf/toc.yml b/DisCatSharp.Docs/pdf/toc.yml new file mode 100644 index 000000000..199b8eae6 --- /dev/null +++ b/DisCatSharp.Docs/pdf/toc.yml @@ -0,0 +1,6 @@ +- name: Articles + href: ../articles/toc.yml +- name: FAQ + href: ../faq.md +- name: API Documentation + href: ../api/toc.yml diff --git a/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs b/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs index 25642d608..472c007d3 100644 --- a/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs +++ b/DisCatSharp.Hosting.Tests/GlobalSuppressions.cs @@ -1,26 +1,29 @@ -// This file is used by Code Analysis to maintain SuppressMessage +// 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/DiscordHostedService.cs b/DisCatSharp.Hosting/DiscordHostedService.cs index ccef9cfa0..ce4028df4 100644 --- a/DisCatSharp.Hosting/DiscordHostedService.cs +++ b/DisCatSharp.Hosting/DiscordHostedService.cs @@ -1,78 +1,90 @@ // 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 /// public abstract class DiscordHostedService : BaseHostedService, IDiscordHostedService { /// public DiscordClient Client { get; protected set; } #pragma warning disable 8618 /// 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) { - + this.Logger = logger; + this.ApplicationLifetime = applicationLifetime; + this.Configuration = config; + this._botSection = configBotSection; + this.ServiceProvider = provider; + this.Initialize(); } + #pragma warning restore 8618 + + /// + /// When the bot fails to start, this method will be invoked. (Default behavior is to shutdown) + /// + /// The exception/reason the bot couldn't start + protected virtual void OnInitializationError(Exception ex) => this.ApplicationLifetime.StopApplication(); 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/Entities/Guild/DiscordMember.cs b/DisCatSharp/Entities/Guild/DiscordMember.cs index 5522d1efc..bc2584018 100644 --- a/DisCatSharp/Entities/Guild/DiscordMember.cs +++ b/DisCatSharp/Entities/Guild/DiscordMember.cs @@ -1,702 +1,731 @@ // 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)}/avatars/{this.GuildAvatarHash}.{(this.GuildAvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; + => 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.Ignore)] + 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); } } /// /// 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. public async Task UpdateVoiceStateAsync(DiscordChannel channel, bool? suppress) { if (channel.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated in a stage channel."); await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, channel.Id, suppress).ConfigureAwait(false); } /// /// Makes the user a speaker. /// /// Thrown when the user is not inside an stage channel. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task MakeSpeakerAsync() { var vs = this.VoiceState; if (vs == null || vs.Channel.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated when the user is inside an stage channel."); await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, vs.Channel.Id, false).ConfigureAwait(false); } /// /// Moves the user to audience. /// /// Thrown when the user is not inside an stage channel. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task MoveToAudienceAsync() { var vs = this.VoiceState; if (vs == null || vs.Channel.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated when the user is inside an stage channel."); await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, vs.Channel.Id, true).ConfigureAwait(false); } /// /// Calculates permissions in a given channel for this member. /// /// Channel to calculate permissions for. /// Calculated permissions for this member in the channel. public Permissions PermissionsIn(DiscordChannel channel) => channel.PermissionsFor(this); /// /// Get's the current member's roles based on the sum of the permissions of their given roles. /// private Permissions GetPermissions() { if (this.Guild.OwnerId == this.Id) return PermissionMethods.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/User/DiscordUser.cs b/DisCatSharp/Entities/User/DiscordUser.cs index 230d1d7cc..45a4465dd 100644 --- a/DisCatSharp/Entities/User/DiscordUser.cs +++ b/DisCatSharp/Entities/User/DiscordUser.cs @@ -1,425 +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 /// /// [JsonIgnore] public bool IsMod => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.DiscordCertifiedModerator); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsPartner => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.DiscordPartner); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsVerifiedBot => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBot); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsBotDev => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBotDeveloper); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsStaff => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.DiscordEmployee); #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/Enums/Permission.cs b/DisCatSharp/Enums/Permission.cs index 690556373..97c4a90d8 100644 --- a/DisCatSharp/Enums/Permission.cs +++ b/DisCatSharp/Enums/Permission.cs @@ -1,357 +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; + internal static Permissions FULL_PERMS { get; } = (Permissions)1099511627775L; // 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, + All = 1099511627775, // 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. + /// + [PermissionString("Time-out Members")] + TimeOutMembers = 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/EventArgs/Application/ApplicationCommandEventArgs.cs b/DisCatSharp/EventArgs/Application/ApplicationCommandEventArgs.cs index 1f4de0005..2cd209b9a 100644 --- a/DisCatSharp/EventArgs/Application/ApplicationCommandEventArgs.cs +++ b/DisCatSharp/EventArgs/Application/ApplicationCommandEventArgs.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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// /// Represents arguments for application command events. /// public sealed class ApplicationCommandEventArgs : DiscordEventArgs { /// /// Gets the command that was modified. /// public DiscordApplicationCommand Command { get; internal set; } /// /// Gets the optional guild of the command. /// public DiscordGuild Guild { get; internal set; } + /// + /// Initializes a new instance of the class. + /// + /// The provider. public ApplicationCommandEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/Application/ApplicationCommandPermissionsUpdateEventArgs.cs b/DisCatSharp/EventArgs/Application/ApplicationCommandPermissionsUpdateEventArgs.cs index 53eaa4481..58643e17b 100644 --- a/DisCatSharp/EventArgs/Application/ApplicationCommandPermissionsUpdateEventArgs.cs +++ b/DisCatSharp/EventArgs/Application/ApplicationCommandPermissionsUpdateEventArgs.cs @@ -1,57 +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 System.Collections.Generic; using DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// /// Represents arguments for application command permissions update events. /// public sealed class ApplicationCommandPermissionsUpdateEventArgs : DiscordEventArgs { /// /// Gets the application command permissions. /// public List Permissions { get; internal set; } /// /// Gets the application command. /// public DiscordApplicationCommand Command { get; internal set; } /// /// Gets the application id. /// public ulong ApplicationId { get; internal set; } /// /// Gets the guild. /// public DiscordGuild Guild { get; internal set; } + /// + /// Initializes a new instance of the class. + /// + /// The provider. public ApplicationCommandPermissionsUpdateEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/Application/GuildApplicationCommandCountEventArgs.cs b/DisCatSharp/EventArgs/Application/GuildApplicationCommandCountEventArgs.cs index 6f5212e69..753f9a143 100644 --- a/DisCatSharp/EventArgs/Application/GuildApplicationCommandCountEventArgs.cs +++ b/DisCatSharp/EventArgs/Application/GuildApplicationCommandCountEventArgs.cs @@ -1,56 +1,60 @@ // 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 application command events. /// public sealed class GuildApplicationCommandCountEventArgs : DiscordEventArgs { /// /// Gets the count of slash commands. /// public int SlashCommands { get; internal set; } /// /// Gets the count of user context menu commands. /// public int UserContextMenuCommands { get; internal set; } /// /// Gets the count of message context menu commands. /// public int MessageContextMenuCommands { get; internal set; } /// /// Gets the guild. /// public DiscordGuild Guild { get; internal set; } + /// + /// Initializes a new instance of the class. + /// + /// The provider. public GuildApplicationCommandCountEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs b/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs index 5738be1e7..c50242a4b 100644 --- a/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs +++ b/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs @@ -1,57 +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 . /// 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/Interaction/InteractionCreateEventArgs.cs b/DisCatSharp/EventArgs/Interaction/InteractionCreateEventArgs.cs index 56526c674..40638d5c7 100644 --- a/DisCatSharp/EventArgs/Interaction/InteractionCreateEventArgs.cs +++ b/DisCatSharp/EventArgs/Interaction/InteractionCreateEventArgs.cs @@ -1,41 +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. using System; using DisCatSharp.Entities; namespace DisCatSharp.EventArgs { /// /// Represents arguments for /// public class InteractionCreateEventArgs : DiscordEventArgs { /// /// Gets the interaction data that was invoked. /// public DiscordInteraction Interaction { get; internal set; } + /// + /// Initializes a new instance of the class. + /// + /// The provider. public InteractionCreateEventArgs(IServiceProvider provider) : base(provider) { } } } diff --git a/DisCatSharp/Net/Abstractions/Transport/TransportMember.cs b/DisCatSharp/Net/Abstractions/Transport/TransportMember.cs index f95a98f2e..14f9d669e 100644 --- a/DisCatSharp/Net/Abstractions/Transport/TransportMember.cs +++ b/DisCatSharp/Net/Abstractions/Transport/TransportMember.cs @@ -1,94 +1,113 @@ // 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 Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// Represents a transport member. /// internal class TransportMember { /// /// Gets the avatar hash. /// [JsonIgnore] public string AvatarHash { get; internal set; } /// /// Gets the guild avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public string GuildAvatarHash { get; internal set; } + /// + /// Gets the guild banner hash. + /// + [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] + public string GuildBannerHash { get; internal set; } + + /// + /// Gets the guild bio. + /// This is not available to bots tho. + /// + [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] + public string GuildBio { get; internal set; } + /// /// Gets the user. /// [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] public TransportUser User { get; internal set; } /// /// Gets the nickname. /// [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] public string Nickname { get; internal set; } /// /// Gets the roles. /// [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] public List Roles { get; internal set; } /// /// Gets the joined at. /// [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] public DateTime JoinedAt { get; internal set; } /// /// Whether this member is deafened. /// [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] public bool IsDeafened { get; internal set; } /// /// Whether this member is muted. /// [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] public bool IsMuted { get; internal set; } /// /// Gets the premium since. /// [JsonProperty("premium_since", NullValueHandling = NullValueHandling.Ignore)] public DateTime? PremiumSince { get; internal set; } /// /// Whether this member is marked as pending. /// [JsonProperty("pending", NullValueHandling = NullValueHandling.Ignore)] public bool? IsPending { get; internal set; } + + /// + /// Gets the timeout time. + /// + [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Ignore)] + public DateTime? CommunicationDisabledUntil { get; internal set; } } } diff --git a/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs b/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs index 9b64d5147..fd621d2ab 100644 --- a/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs +++ b/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs @@ -1,149 +1,157 @@ // 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.Net.Abstractions { /// /// Represents a transport user. /// internal class TransportUser { /// /// Gets the id. /// [JsonProperty("id")] public ulong Id { get; internal set; } /// /// Gets the username. /// [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] public string Username { get; internal set; } /// /// Gets or sets the discriminator. /// [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] internal string Discriminator { get; set; } /// /// Gets the username with discriminator. /// internal string UsernameWithDiscriminator => $"{this.Username}#{this.Discriminator}"; /// /// Gets the avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public string AvatarHash { get; internal set; } /// /// Gets the banner hash. /// [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] public string BannerHash { get; internal set; } /// /// Gets the banner color. /// [JsonProperty("accent_color")] public int? BannerColor { get; internal set; } /// /// Gets a value indicating whether is bot. /// [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] public bool IsBot { get; internal set; } /// /// Gets a value indicating whether mfa enabled. /// [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? MfaEnabled { get; internal set; } /// /// Gets a value indicating whether verified. /// [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] public bool? Verified { get; internal set; } /// /// Gets the email. /// [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] public string Email { get; internal set; } /// /// Gets the premium type. /// [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] public PremiumType? PremiumType { get; internal set; } /// /// Gets the locale. /// [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] public string Locale { get; internal set; } /// /// Gets the OAuth flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public UserFlags? OAuthFlags { get; internal set; } /// /// Gets the flags. /// [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] public UserFlags? Flags { get; internal set; } + /// + /// Gets the users bio. + /// This is not available to bots tho. + /// + [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] + public string Bio { get; internal set; } + /// /// Initializes a new instance of the class. /// internal TransportUser() { } /// /// Initializes a new instance of the class from an existing . /// /// The other transport user. internal TransportUser(TransportUser other) { this.Id = other.Id; this.Username = other.Username; this.Discriminator = other.Discriminator; this.AvatarHash = other.AvatarHash; this.BannerHash = other.BannerHash; this.BannerColor = other.BannerColor; this.IsBot = other.IsBot; this.MfaEnabled = other.MfaEnabled; this.Verified = other.Verified; this.Email = other.Email; this.PremiumType = other.PremiumType; this.Locale = other.Locale; this.Flags = other.Flags; this.OAuthFlags = other.OAuthFlags; + this.Bio = other.Bio; } } } diff --git a/DisCatSharp/Net/WebSocket/WebSocketClient.cs b/DisCatSharp/Net/WebSocket/WebSocketClient.cs index 54916243e..cc679399a 100644 --- a/DisCatSharp/Net/WebSocket/WebSocketClient.cs +++ b/DisCatSharp/Net/WebSocket/WebSocketClient.cs @@ -1,413 +1,416 @@ // 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.WebSockets; using System.Threading; using System.Threading.Tasks; using DisCatSharp.EventArgs; using DisCatSharp.Common.Utilities; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.Net.WebSocket { // weebsocket // not even sure whether emzi or I posted this. much love, naam. /// /// The default, native-based WebSocket client implementation. /// public class WebSocketClient : IWebSocketClient { /// /// The outgoing chunk size. /// private const int OutgoingChunkSize = 8192; // 8 KiB /// /// The incoming chunk size. /// private const int IncomingChunkSize = 32768; // 32 KiB /// /// Gets the proxy settings for this client. /// public IWebProxy Proxy { get; } /// /// Gets the collection of default headers to send when connecting to the remote endpoint. /// public IReadOnlyDictionary DefaultHeaders { get; } + /// + /// Gets or sets the service provider. + /// IServiceProvider IWebSocketClient.ServiceProvider { get => this._serviceProvider; set => this._serviceProvider = value; } private readonly Dictionary _defaultHeaders; private Task _receiverTask; private CancellationTokenSource _receiverTokenSource; private CancellationToken _receiverToken; private readonly SemaphoreSlim _senderLock; private CancellationTokenSource _socketTokenSource; private CancellationToken _socketToken; private ClientWebSocket _ws; private volatile bool _isClientClose = false; private volatile bool _isConnected = false; private bool _isDisposed = false; /// /// Instantiates a new WebSocket client with specified proxy settings. /// /// Proxy settings for the client. /// Service provider. private WebSocketClient(IWebProxy proxy, IServiceProvider provider) { this._connected = new AsyncEvent("WS_CONNECT", TimeSpan.Zero, this.EventErrorHandler); this._disconnected = new AsyncEvent("WS_DISCONNECT", TimeSpan.Zero, this.EventErrorHandler); this._messageReceived = new AsyncEvent("WS_MESSAGE", TimeSpan.Zero, this.EventErrorHandler); this._exceptionThrown = new AsyncEvent("WS_ERROR", TimeSpan.Zero, null); this.Proxy = proxy; this._defaultHeaders = new Dictionary(); this.DefaultHeaders = new ReadOnlyDictionary(this._defaultHeaders); this._receiverTokenSource = null; this._receiverToken = CancellationToken.None; this._senderLock = new SemaphoreSlim(1); this._socketTokenSource = null; this._socketToken = CancellationToken.None; this._serviceProvider = provider; } /// /// Connects to a specified remote WebSocket endpoint. /// /// The URI of the WebSocket endpoint. public async Task ConnectAsync(Uri uri) { // Disconnect first try { await this.DisconnectAsync().ConfigureAwait(false); } catch { } // Disallow sending messages await this._senderLock.WaitAsync().ConfigureAwait(false); try { // This can be null at this point this._receiverTokenSource?.Dispose(); this._socketTokenSource?.Dispose(); this._ws?.Dispose(); this._ws = new ClientWebSocket(); this._ws.Options.Proxy = this.Proxy; this._ws.Options.KeepAliveInterval = TimeSpan.Zero; if (this._defaultHeaders != null) foreach (var (k, v) in this._defaultHeaders) this._ws.Options.SetRequestHeader(k, v); this._receiverTokenSource = new CancellationTokenSource(); this._receiverToken = this._receiverTokenSource.Token; this._socketTokenSource = new CancellationTokenSource(); this._socketToken = this._socketTokenSource.Token; this._isClientClose = false; this._isDisposed = false; await this._ws.ConnectAsync(uri, this._socketToken).ConfigureAwait(false); this._receiverTask = Task.Run(this.ReceiverLoopAsync, this._receiverToken); } finally { this._senderLock.Release(); } } /// /// Disconnects the WebSocket connection. /// /// The code /// The message /// Lala Sabathil,06.07.2021 /// Lala Sabathil,06.07.2021 public async Task DisconnectAsync(int code = 1000, string message = "") { // Ensure that messages cannot be sent await this._senderLock.WaitAsync().ConfigureAwait(false); try { this._isClientClose = true; if (this._ws != null && (this._ws.State == WebSocketState.Open || this._ws.State == WebSocketState.CloseReceived)) await this._ws.CloseOutputAsync((WebSocketCloseStatus)code, message, CancellationToken.None).ConfigureAwait(false); if (this._receiverTask != null) await this._receiverTask.ConfigureAwait(false); // Ensure that receiving completed if (this._isConnected) this._isConnected = false; if (!this._isDisposed) { // Cancel all running tasks if (this._socketToken.CanBeCanceled) this._socketTokenSource?.Cancel(); this._socketTokenSource?.Dispose(); if (this._receiverToken.CanBeCanceled) this._receiverTokenSource?.Cancel(); this._receiverTokenSource?.Dispose(); this._isDisposed = true; } } catch { } finally { this._senderLock.Release(); } } /// /// Send a message to the WebSocket server. /// /// The message to send. public async Task SendMessageAsync(string message) { if (this._ws == null) return; if (this._ws.State != WebSocketState.Open && this._ws.State != WebSocketState.CloseReceived) return; var bytes = Utilities.UTF8.GetBytes(message); await this._senderLock.WaitAsync().ConfigureAwait(false); try { var len = bytes.Length; var segCount = len / OutgoingChunkSize; if (len % OutgoingChunkSize != 0) segCount++; for (var i = 0; i < segCount; i++) { var segStart = OutgoingChunkSize * i; var segLen = Math.Min(OutgoingChunkSize, len - segStart); await this._ws.SendAsync(new ArraySegment(bytes, segStart, segLen), WebSocketMessageType.Text, i == segCount - 1, CancellationToken.None).ConfigureAwait(false); } } finally { this._senderLock.Release(); } } /// /// Adds a header to the default header collection. /// /// Name of the header to add. /// Value of the header to add. /// Whether the operation succeeded. public bool AddDefaultHeader(string name, string value) { this._defaultHeaders[name] = value; return true; } /// /// Removes a header from the default header collection. /// /// Name of the header to remove. /// Whether the operation succeeded. public bool RemoveDefaultHeader(string name) => this._defaultHeaders.Remove(name); /// /// Disposes of resources used by this WebSocket client instance. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; this.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); this._receiverTokenSource?.Dispose(); this._socketTokenSource?.Dispose(); } /// /// Receivers the loop. /// internal async Task ReceiverLoopAsync() { await Task.Yield(); var token = this._receiverToken; var buffer = new ArraySegment(new byte[IncomingChunkSize]); try { using var bs = new MemoryStream(); while (!token.IsCancellationRequested) { // See https://github.com/RogueException/Discord.Net/commit/ac389f5f6823e3a720aedd81b7805adbdd78b66d // for explanation on the cancellation token WebSocketReceiveResult result; byte[] resultBytes; do { result = await this._ws.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); if (result.MessageType == WebSocketMessageType.Close) break; bs.Write(buffer.Array, 0, result.Count); } while (!result.EndOfMessage); resultBytes = new byte[bs.Length]; bs.Position = 0; bs.Read(resultBytes, 0, resultBytes.Length); bs.Position = 0; bs.SetLength(0); if (!this._isConnected && result.MessageType != WebSocketMessageType.Close) { this._isConnected = true; await this._connected.InvokeAsync(this, new SocketEventArgs(this._serviceProvider)).ConfigureAwait(false); } if (result.MessageType == WebSocketMessageType.Binary) { await this._messageReceived.InvokeAsync(this, new SocketBinaryMessageEventArgs(resultBytes)).ConfigureAwait(false); } else if (result.MessageType == WebSocketMessageType.Text) { await this._messageReceived.InvokeAsync(this, new SocketTextMessageEventArgs(Utilities.UTF8.GetString(resultBytes))).ConfigureAwait(false); } else // close { if (!this._isClientClose) { var code = result.CloseStatus.Value; code = code == WebSocketCloseStatus.NormalClosure || code == WebSocketCloseStatus.EndpointUnavailable ? (WebSocketCloseStatus)4000 : code; await this._ws.CloseOutputAsync(code, result.CloseStatusDescription, CancellationToken.None).ConfigureAwait(false); } await this._disconnected.InvokeAsync(this, new SocketCloseEventArgs(this._serviceProvider) { CloseCode = (int)result.CloseStatus, CloseMessage = result.CloseStatusDescription }).ConfigureAwait(false); break; } } } catch (Exception ex) { await this._exceptionThrown.InvokeAsync(this, new SocketErrorEventArgs(this._serviceProvider) { Exception = ex }).ConfigureAwait(false); await this._disconnected.InvokeAsync(this, new SocketCloseEventArgs(this._serviceProvider) { CloseCode = -1, CloseMessage = "" }).ConfigureAwait(false); } // Don't await or you deadlock // DisconnectAsync waits for this method _ = this.DisconnectAsync().ConfigureAwait(false); } /// /// Creates a new instance of . /// /// Proxy to use for this client instance. /// Service provider. /// An instance of . public static IWebSocketClient CreateNew(IWebProxy proxy, IServiceProvider provider) => new WebSocketClient(proxy, provider); #region Events /// /// Triggered when the client connects successfully. /// public event AsyncEventHandler Connected { add => this._connected.Register(value); remove => this._connected.Unregister(value); } private readonly AsyncEvent _connected; /// /// Triggered when the client is disconnected. /// public event AsyncEventHandler Disconnected { add => this._disconnected.Register(value); remove => this._disconnected.Unregister(value); } private readonly AsyncEvent _disconnected; /// /// Triggered when the client receives a message from the remote party. /// public event AsyncEventHandler MessageReceived { add => this._messageReceived.Register(value); remove => this._messageReceived.Unregister(value); } private readonly AsyncEvent _messageReceived; /// /// Triggered when an error occurs in the client. /// public event AsyncEventHandler ExceptionThrown { add => this._exceptionThrown.Register(value); remove => this._exceptionThrown.Unregister(value); } private readonly AsyncEvent _exceptionThrown; private IServiceProvider _serviceProvider; /// /// Events the error handler. /// /// The event. /// The exeption. /// The handler. /// The sender. /// The event args. private void EventErrorHandler(AsyncEvent asyncEvent, Exception ex, AsyncEventHandler handler, WebSocketClient sender, TArgs eventArgs) where TArgs : AsyncEventArgs => this._exceptionThrown.InvokeAsync(this, new SocketErrorEventArgs(this._serviceProvider) { Exception = ex }).ConfigureAwait(false).GetAwaiter().GetResult(); #endregion } } diff --git a/rebuild-docs.ps1 b/rebuild-docs.ps1 index 7ab015b45..75e3e6033 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. # # 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_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 }