diff --git a/Exiled.API/Extensions/StringExtensions.cs b/Exiled.API/Extensions/StringExtensions.cs index 093824c640..30afc520b2 100644 --- a/Exiled.API/Extensions/StringExtensions.cs +++ b/Exiled.API/Extensions/StringExtensions.cs @@ -87,6 +87,31 @@ public static string ToSnakeCase(this string str, bool shouldReplaceSpecialChars return shouldReplaceSpecialChars ? Regex.Replace(snakeCaseString, @"[^0-9a-zA-Z_]+", string.Empty) : snakeCaseString; } + /// + /// Converts a from snake_case convention. + /// + /// The string to be converted. + /// Returns the new NotSnakeCase string. + public static string FromSnakeCase(this string str) + { + string result = string.Empty; + + for (int i = 0; i < str.Length; i++) + { + if (str[i] == '_') + { + result += str[i + 1].ToString().ToUpper(); + i++; + } + else + { + result += str[i]; + } + } + + return result; + } + /// /// Converts an into a string. /// diff --git a/Exiled.Events/Commands/ConfigValue/ConfigValue.cs b/Exiled.Events/Commands/ConfigValue/ConfigValue.cs new file mode 100644 index 0000000000..e2e8ee2a28 --- /dev/null +++ b/Exiled.Events/Commands/ConfigValue/ConfigValue.cs @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Commands.ConfigValue +{ + using System; + + using CommandSystem; + + /// + /// The config value command. + /// + [CommandHandler(typeof(RemoteAdminCommandHandler))] + [CommandHandler(typeof(GameConsoleCommandHandler))] + public class ConfigValue : ParentCommand + { + /// + /// Initializes a new instance of the class. + /// + public ConfigValue() + { + LoadGeneratedCommands(); + } + + /// + public override string Command { get; } = "config_value"; + + /// + public override string[] Aliases { get; } = { "value", "cv", "cfgval" }; + + /// + public override string Description { get; } = "Gets or sets a config value"; + + /// + public override void LoadGeneratedCommands() + { + RegisterCommand(Get.Instance); + RegisterCommand(Set.Instance); + } + + /// + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + response = "Please, specify a valid subcommand! Available ones: get, set"; + return false; + } + } +} \ No newline at end of file diff --git a/Exiled.Events/Commands/ConfigValue/Get.cs b/Exiled.Events/Commands/ConfigValue/Get.cs new file mode 100644 index 0000000000..639f044e6b --- /dev/null +++ b/Exiled.Events/Commands/ConfigValue/Get.cs @@ -0,0 +1,88 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Commands.ConfigValue +{ + using System; + using System.Linq; + using System.Reflection; + + using CommandSystem; + using Exiled.API.Extensions; + using Exiled.API.Interfaces; + using Exiled.Permissions.Extensions; + using RemoteAdmin; + + /// + /// The command to get config value. + /// + public class Get : ICommand + { + /// + /// Gets a static instance of class. + /// + public static Get Instance { get; } = new(); + + /// + public string Command { get; } = "get"; + + /// + public string[] Aliases { get; } = { "print" }; + + /// + public string Description { get; } = "Gets a config value"; + + /// + public bool SanitizeResponse { get; } + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + const string perm = "cv.get"; + + if (!sender.CheckPermission(perm) && sender is PlayerCommandSender playerSender && !playerSender.FullPermissions) + { + response = $"You can't get a config value, you don't have \"{perm}\" permissions."; + return false; + } + + if (arguments.Count != 1) + { + response = "Please, use: cv get PluginName.ValueName"; + return false; + } + + string[] args = arguments.At(0).Split('.'); + + string pluginName = args[0]; + string propertyName = args[1].FromSnakeCase(); + + if (Loader.Loader.Plugins.All(x => x.Name != pluginName)) + { + response = $"Plugin not found: {pluginName}"; + return false; + } + + IPlugin plugin = Loader.Loader.Plugins.First(x => x.Name == pluginName); + + if (plugin.Config == null) + { + response = "Plugin config is null!"; + return false; + } + + if (plugin.Config.GetType().GetProperty(propertyName) is not PropertyInfo property) + { + response = $"Config value not found: {propertyName} ({propertyName.ToSnakeCase()})"; + return false; + } + + response = $"Value: {property.GetValue(plugin.Config)}"; + return true; + } + } +} \ No newline at end of file diff --git a/Exiled.Events/Commands/ConfigValue/Set.cs b/Exiled.Events/Commands/ConfigValue/Set.cs new file mode 100644 index 0000000000..e0bc489de9 --- /dev/null +++ b/Exiled.Events/Commands/ConfigValue/Set.cs @@ -0,0 +1,115 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Commands.ConfigValue +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + using CommandSystem; + using Exiled.API.Extensions; + using Exiled.API.Features; + using Exiled.API.Interfaces; + using Exiled.Loader; + using Exiled.Permissions.Extensions; + using RemoteAdmin; + + /// + /// The command to set config value. + /// + public class Set : ICommand + { + /// + /// Gets a static instance of class. + /// + public static Set Instance { get; } = new(); + + /// + public string Command { get; } = "set"; + + /// + public string[] Aliases { get; } = { "edit" }; + + /// + public string Description { get; } = "Sets a config value"; + + /// + public bool SanitizeResponse { get; } + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + const string perm = "cv.set"; + + if (!sender.CheckPermission(perm) && sender is PlayerCommandSender playerSender && !playerSender.FullPermissions) + { + response = $"You can't set a config value, you don't have \"{perm}\" permissions."; + return false; + } + + if (arguments.Count != 2) + { + response = "Please, use: cv set PluginName.ValueName NewValue"; + return false; + } + + string[] args = arguments.At(0).Split('.'); + + string pluginName = args[0]; + string propertyName = args[1].FromSnakeCase(); + + if (Loader.Plugins.All(x => x.Name != pluginName)) + { + response = $"Plugin not found: {pluginName}"; + return false; + } + + IPlugin plugin = Loader.GetPlugin(pluginName); + + if (plugin.Config == null) + { + response = "Plugin config is null!"; + return false; + } + + if (plugin.Config.GetType().GetProperty(propertyName) is not PropertyInfo property) + { + response = $"Config value not found: {propertyName} ({propertyName.ToSnakeCase()})"; + return false; + } + + object newValue; + + try + { + newValue = Convert.ChangeType(arguments.At(1), property.PropertyType); + } + catch (Exception exception) + { + Log.Error(exception); + + response = $"Provided value is not a type of {property.PropertyType.Name}"; + return false; + } + + if (newValue == null) + { + response = $"Provided value is not a type of {property.PropertyType.Name}"; + return false; + } + + SortedDictionary configs = ConfigManager.LoadSorted(ConfigManager.Read()); + property.SetValue(configs[pluginName], newValue); + bool success = ConfigManager.Save(configs); + + response = success ? "Value has been successfully changed and added in config" : "Value has been successfully changed but not added in config"; + return true; + } + } +} \ No newline at end of file diff --git a/Exiled.Events/Commands/PluginManager/Install.cs b/Exiled.Events/Commands/PluginManager/Install.cs new file mode 100644 index 0000000000..641be491b7 --- /dev/null +++ b/Exiled.Events/Commands/PluginManager/Install.cs @@ -0,0 +1,189 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Commands.PluginManager +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.IO.Compression; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Threading.Tasks; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.Events.Features; + using Exiled.Permissions.Extensions; + using RemoteAdmin; + using Utf8Json; + + /// + /// The command to install plugin. + /// + public class Install : ICommand + { + /// + /// Gets the class instance. + /// + public static Install Instance { get; } = new(); + + /// + public string Command { get; } = "install"; + + /// + public string[] Aliases { get; } = { "download", "inst", "dwnl" }; + + /// + public string Description { get; } = "Installs a plugin"; + + /// + public bool SanitizeResponse { get; } + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + const string perm = "pm.install"; + + if (!sender.CheckPermission(perm) && sender is PlayerCommandSender playerSender && !playerSender.FullPermissions) + { + response = $"You can't install a plugin, you don't have \"{perm}\" permissions."; + return false; + } + + if (arguments.Count is < 1 or > 4) + { + response = "Please, use: pluginmanager install [PluginAuthor/PluginName OR Verified plugin name] {File Name} {String version} {Dependencies string version}"; + return false; + } + + string downloadPath = string.Empty; + string dependenciesPath = string.Empty; + + if (arguments.Count == 1) + { + List list; + try + { + using HttpClient client = new(); + using HttpResponseMessage responseMessage = client.GetAsync("https://exiled.to/api/plugins").Result; + string res = responseMessage.Content.ReadAsStringAsync().Result; + list = JsonSerializer.Deserialize>(res); + } + catch (Exception ex) + { + Log.Error(ex); + response = null; + return false; + } + + VerifiedPlugin plugin = list.FirstOrDefault(x => (int.TryParse(arguments.At(0), out int id) && x.id == id) || string.Equals(x.name, arguments.At(0), StringComparison.CurrentCultureIgnoreCase)); + + if (plugin == null && !Uri.TryCreate(arguments.At(0), UriKind.RelativeOrAbsolute, out _)) + { + response = $"There is no verified plugin with such id or name: {arguments.At(0)}"; + return false; + } + + if (plugin != null) + { + downloadPath = plugin.repository + $"/releases/latest/download/{plugin.fileName}"; + dependenciesPath = plugin.repository + "/releases/latest/download/dependencies.zip"; + } + } + + string fileName = arguments.At(0).Split('/')[1]; + + if (arguments.Count > 1) + { + fileName = arguments.At(1); + } + + string targetRelease = "latest"; + + if (arguments.Count > 2) + { + targetRelease = $"tag/{arguments.At(2)}"; + } + + if (downloadPath == string.Empty) + downloadPath = $"https://github.com/{arguments.At(0)}/releases/{targetRelease}/download/{fileName}.dll"; + + try + { + using WebClient client = new(); + client.Headers.Add("User-Agent", $"Exiled.Events-{Events.Instance.Version}"); + client.DownloadFile(downloadPath, Path.Combine(Paths.Plugins, $"{fileName}.dll")); + } + catch (WebException webException) + { + if (webException.Response is HttpWebResponse httpResponse) + { + if (httpResponse.StatusCode == HttpStatusCode.NotFound) + { + response = $"Returned error 404: Path not found - https://github.com/{arguments.At(0)}/releases/{targetRelease} or file was not found - {fileName}.dll"; + return false; + } + + response = $"Returned error {(int)httpResponse.StatusCode}: {webException.Message}"; + return false; + } + } + + if (arguments.Count > 3) + { + targetRelease = $"tag/{arguments.At(3)}"; + } + + if (dependenciesPath == string.Empty) + dependenciesPath = $"https://github.com/{arguments.At(0)}/releases/{targetRelease}/download/dependencies.zip"; + + try + { + using HttpClient httpClient = new(); + httpClient.DefaultRequestHeaders.Add("User-Agent", $"Exiled.Events-{Events.Instance.Version}"); + + using HttpResponseMessage downloadResult = httpClient.GetAsync(dependenciesPath).Result; + using Task downloadArchiveStream = downloadResult.Content.ReadAsStreamAsync(); + + using ZipArchive archive = new(downloadArchiveStream.GetAwaiter().GetResult()); + + foreach (ZipArchiveEntry entry in archive.Entries) + { + FileStream fileStream = File.Create(Path.Combine(Paths.Dependencies, entry.Name)); + entry.Open().CopyTo(fileStream); + fileStream.Close(); + } + } + catch (InvalidDataException) + { + response = $"Successfully installed {fileName}.dll without dependencies!"; + return true; + } + catch (WebException webException) + { + if (webException.Response is HttpWebResponse httpResponse && httpResponse.StatusCode != HttpStatusCode.NotFound) + { + response = $"Returned error {(int)httpResponse.StatusCode}: {webException.Message}"; + return false; + } + + response = $"Successfully installed {fileName}.dll without dependencies!\nDependencies path - {dependenciesPath}"; + return true; + } + catch (Exception exception) + { + response = exception.ToString(); + return false; + } + + response = $"Successfully installed {fileName}.dll and it's dependencies!"; + return true; + } + } +} \ No newline at end of file diff --git a/Exiled.Events/Commands/PluginManager/PluginManager.cs b/Exiled.Events/Commands/PluginManager/PluginManager.cs index ba778d5f43..77805b59ab 100644 --- a/Exiled.Events/Commands/PluginManager/PluginManager.cs +++ b/Exiled.Events/Commands/PluginManager/PluginManager.cs @@ -33,7 +33,7 @@ public PluginManager() public override string[] Aliases { get; } = new[] { "plymanager", "plmanager", "pmanager", "plym" }; /// - public override string Description { get; } = "Manage plugin. Enable, disable and show plugins."; + public override string Description { get; } = "Manage plugin. Enable, disable, show and install plugins."; /// public override void LoadGeneratedCommands() @@ -42,12 +42,14 @@ public override void LoadGeneratedCommands() RegisterCommand(Enable.Instance); RegisterCommand(Disable.Instance); RegisterCommand(Patches.Instance); + RegisterCommand(Install.Instance); + RegisterCommand(Plugins.Instance); } /// protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) { - response = "Please, specify a valid subcommand! Available ones: enable, disable, show, patches"; + response = "Please, specify a valid subcommand! Available ones: enable, disable, show, patches, install"; return false; } } diff --git a/Exiled.Events/Commands/PluginManager/Plugins.cs b/Exiled.Events/Commands/PluginManager/Plugins.cs new file mode 100644 index 0000000000..049c00ed0c --- /dev/null +++ b/Exiled.Events/Commands/PluginManager/Plugins.cs @@ -0,0 +1,66 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Commands.PluginManager +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + + using CommandSystem; + using Exiled.API.Features; + using Exiled.API.Interfaces; + using Exiled.Events.Features; + using Utf8Json; + + /// + /// A command to display list with all verified plugin. + /// + public class Plugins : IPermissioned, ICommand + { + /// + /// Gets the static instance of the command. + /// + public static Plugins Instance { get; } = new(); + + /// + public string Command { get; } = "plugins"; + + /// + public string[] Aliases { get; } = Array.Empty(); + + /// + public string Description { get; } = "Gets a list of all verified plugins."; + + /// + public bool SanitizeResponse { get; } + + /// + public string Permission { get; } = "pm.plugins"; + + /// + public bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + try + { + using HttpClient client = new(); + using HttpResponseMessage responseMessage = client.GetAsync("https://exiled.to/api/plugins").Result; + string res = responseMessage.Content.ReadAsStringAsync().Result; + List list = JsonSerializer.Deserialize>(res); + response = "- " + string.Join("- ", list.Select(x => x.ToString())); + return true; + } + catch (Exception ex) + { + Log.Error(ex); + response = null; + return false; + } + } + } +} \ No newline at end of file diff --git a/Exiled.Events/Exiled.Events.csproj b/Exiled.Events/Exiled.Events.csproj index 421dc0108a..681a88588a 100644 --- a/Exiled.Events/Exiled.Events.csproj +++ b/Exiled.Events/Exiled.Events.csproj @@ -20,6 +20,7 @@ + @@ -30,6 +31,7 @@ + diff --git a/Exiled.Events/Features/VerifiedPlugin.cs b/Exiled.Events/Features/VerifiedPlugin.cs new file mode 100644 index 0000000000..06b9f51ecd --- /dev/null +++ b/Exiled.Events/Features/VerifiedPlugin.cs @@ -0,0 +1,60 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Exiled Team. All rights reserved. +// Licensed under the CC BY-SA 3.0 license. +// +// ----------------------------------------------------------------------- + +namespace Exiled.Events.Features +{ +#pragma warning disable SA1300 + + /// + /// A class with information about a verified plugin. + /// + internal class VerifiedPlugin + { + /// + /// Initializes a new instance of the class. + /// + public VerifiedPlugin() + { + } + + /// + /// Gets or sets the plugin id. + /// + public int id { get; set; } + + /// + /// Gets or sets the plugin name. + /// + public string name { get; set; } + + /// + /// Gets or sets the plugin author. + /// + public string author { get; set; } + + /// + /// Gets or sets the plugin description. + /// + public string description { get; set; } + + /// + /// Gets or sets the URL to plugin repository. + /// + public string repository { get; set; } + + /// + /// Gets or sets name of the file that we should download. + /// + public string fileName { get; set; } + + /// + /// Returns a verified plugin in a human-readable format. + /// + /// Human-readable string. + public override string ToString() => $"Plugin ID: {id}\nPlugin Name: {name}\nPlugin Author: {author}\nPlugin Description: {description}\n"; + } +} \ No newline at end of file