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