implement plugins section

This commit is contained in:
zznty
2022-10-04 19:31:59 +07:00
parent 7136a93c76
commit 1e29719125
9 changed files with 165 additions and 3 deletions

View File

@@ -0,0 +1,8 @@
namespace TorchRemote.Models.Responses;
public record PluginInfo(Guid Id, string Name, string Version);
public record PluginItemInfo(Guid Id, string Name, string Version, string Author) : PluginInfo(Id, Name, Version);
public record FullPluginItemInfo(Guid Id, string Name, string Description, string Version, string Author) : PluginItemInfo(Id, Name, Version, Author);
public record InstalledPluginInfo(Guid Id, string Name, string Version, string? SettingId) : PluginInfo(Id, Name, Version);

View File

@@ -0,0 +1,49 @@
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using Torch.API.WebAPI;
using TorchRemote.Models.Responses;
using TorchRemote.Plugin.Utils;
namespace TorchRemote.Plugin.Controllers;
public class PluginDownloadsController : WebApiController
{
private const string RootPath = "/plugins/downloads";
[Route(HttpVerbs.Get, RootPath)]
public async Task<IEnumerable<PluginInfo>> GetAsync()
{
var response = await PluginQuery.Instance.QueryAll();
return response.Plugins.Select(b => new PluginItemInfo(Guid.Parse(b.ID), b.Name, b.LatestVersion, b.Author));
}
[Route(HttpVerbs.Get, $"{RootPath}/{{id}}")]
public async Task<FullPluginItemInfo> GetFullAsync(Guid id)
{
var response = await PluginQuery.Instance.QueryOne(id);
if (response is null)
throw HttpException.NotFound("Plugin not found", id);
return new(Guid.Parse(response.ID), response.Name, response.Description, response.LatestVersion,
response.Author);
}
[Route(HttpVerbs.Post, $"{RootPath}/{{id}}/install")]
public async Task InstallAsync(Guid id)
{
if (Statics.PluginManager.Plugins.ContainsKey(id))
throw HttpException.BadRequest("Plugin with given id already exists", id);
var response = await PluginQuery.Instance.QueryOne(id);
if (response is null)
throw HttpException.NotFound("Plugin not found", id);
if (!await PluginQuery.Instance.DownloadPlugin(response))
throw HttpException.InternalServerError();
Statics.Torch.Config.Plugins.Add(id);
}
}

View File

@@ -0,0 +1,66 @@
using System.IO;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using HttpMultipartParser;
using Swan;
using Torch.API.Managers;
using Torch.Managers;
using TorchRemote.Models.Responses;
using TorchRemote.Plugin.Utils;
namespace TorchRemote.Plugin.Controllers;
public class PluginsController : WebApiController
{
private const string RootPath = "/plugins";
[Route(HttpVerbs.Get, RootPath)]
public IEnumerable<InstalledPluginInfo> Get()
{
return Statics.PluginManager.Select(plugin => new InstalledPluginInfo(plugin.Id, plugin.Name, plugin.Version,
Statics.SettingManager.PluginSettings!
.GetValueOrDefault(plugin.Id)));
}
[Route(HttpVerbs.Delete, $"{RootPath}/{{id}}")]
public void Uninstall(Guid id)
{
foreach (var zip in Directory.EnumerateFiles(Statics.PluginManager.PluginDir, "*.zip"))
{
var manifest = PluginManifestUtils.ReadFromZip(zip);
if (manifest.Guid != id)
continue;
File.Delete(zip);
return;
}
throw HttpException.NotFound("Plugin zip with given id not found", id);
}
[Route(HttpVerbs.Put, RootPath)]
public async Task<IEnumerable<PluginInfo>> InstallAsync()
{
var payload = await MultipartFormDataParser.ParseAsync(Request.InputStream);
var pluginsToInstall = payload.Files.ToDictionary(f => PluginManifestUtils.ReadFromZip(f.Data));
if (pluginsToInstall.Keys.FirstOrDefault(m => Statics.PluginManager.Plugins.ContainsKey(m.Guid)) is { } m)
throw HttpException.BadRequest("Plugin with given id already exists", m.Guid);
return pluginsToInstall.Select(b =>
{
var (manifest, file) = b;
var path = Path.Combine(Statics.PluginManager.PluginDir, $"{manifest.Name}.zip");
using var zipStream = File.Create(path);
file.Data.Position = 0;
file.Data.CopyTo(zipStream);
return new PluginInfo(manifest.Guid, manifest.Name, manifest.Version);
});
}
}

View File

@@ -58,7 +58,9 @@ public class ApiServerManager : Manager
.WithController<ServerController>() .WithController<ServerController>()
.WithController<SettingsController>() .WithController<SettingsController>()
.WithController<WorldsController>() .WithController<WorldsController>()
.WithController<ChatController>()) .WithController<ChatController>()
.WithController<PluginsController>()
.WithController<PluginDownloadsController>())
.WithModule(new LogsModule("/api/live/logs", true)) .WithModule(new LogsModule("/api/live/logs", true))
.WithModule(chatModule) .WithModule(chatModule)
.WithBearerToken("/api", new SymmetricSecurityKey(Convert.FromBase64String(_config.SecurityKey)), new BasicAuthorizationServerProvider()); .WithBearerToken("/api", new SymmetricSecurityKey(Convert.FromBase64String(_config.SecurityKey)), new BasicAuthorizationServerProvider());

View File

@@ -70,8 +70,11 @@ public class SettingManager : Manager
var persistentType = persistentInstance.GetType(); var persistentType = persistentInstance.GetType();
var getter = persistentType.GetProperty("Data")!; var getter = persistentType.GetProperty("Data")!;
var settingType = persistentType.GenericTypeArguments[0];
RegisterSetting(plugin.Name, getter.GetValue(persistentInstance), persistentType.GenericTypeArguments[0]); RegisterSetting(plugin.Name, getter.GetValue(persistentInstance), settingType);
PluginSettings.Add(plugin.Id, settingType.FullName!);
} }
} }
@@ -92,6 +95,7 @@ public class SettingManager : Manager
} }
public IDictionary<string, Setting> Settings { get; } = new ConcurrentDictionary<string, Setting>(); public IDictionary<string, Setting> Settings { get; } = new ConcurrentDictionary<string, Setting>();
public IDictionary<Guid, string> PluginSettings { get; } = new ConcurrentDictionary<Guid, string>();
} }
public record Setting(string Name, Type Type, JsonSchema Schema, object Value); public record Setting(string Name, Type Type, JsonSchema Schema, object Value);

View File

@@ -35,6 +35,7 @@
<HintPath>$(TorchDir)DedicatedServer64\System.Memory.dll</HintPath> <HintPath>$(TorchDir)DedicatedServer64\System.Memory.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="System.IO.Compression" />
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
<Reference Include="Torch"> <Reference Include="Torch">
<HintPath>$(TorchDir)Torch.dll</HintPath> <HintPath>$(TorchDir)Torch.dll</HintPath>
@@ -77,6 +78,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EmbedIO" Version="3.4.3" /> <PackageReference Include="EmbedIO" Version="3.4.3" />
<PackageReference Include="EmbedIO.BearerToken" Version="3.4.2" /> <PackageReference Include="EmbedIO.BearerToken" Version="3.4.2" />
<PackageReference Include="HttpMultipartParser" Version="7.0.0" />
<PackageReference Include="Json.More.Net" Version="1.7.1" /> <PackageReference Include="Json.More.Net" Version="1.7.1" />
<PackageReference Include="JsonPointer.Net" Version="2.2.2" /> <PackageReference Include="JsonPointer.Net" Version="2.2.2" />
<PackageReference Include="JsonSchema.Net" Version="3.2.2" /> <PackageReference Include="JsonSchema.Net" Version="3.2.2" />

View File

@@ -0,0 +1,29 @@
using System.IO;
using System.IO.Compression;
using System.Xml.Serialization;
using Torch;
namespace TorchRemote.Plugin.Utils;
public static class PluginManifestUtils
{
private static readonly XmlSerializer Serializer = new(typeof(PluginManifest));
public static PluginManifest Read(Stream stream)
{
return (PluginManifest)Serializer.Deserialize(stream);
}
public static PluginManifest ReadFromZip(Stream stream)
{
using var archive = new ZipArchive(stream);
using var entryStream = archive.GetEntry("manifest.xml")!.Open();
return Read(entryStream);
}
public static PluginManifest ReadFromZip(string archivePath)
{
using var stream = File.OpenRead(archivePath);
return ReadFromZip(stream);
}
}

View File

@@ -3,6 +3,7 @@ using Torch;
using Torch.API; using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.Commands; using Torch.Commands;
using Torch.Managers;
using Torch.Server; using Torch.Server;
using Torch.Server.Managers; using Torch.Server.Managers;
using TorchRemote.Plugin.Managers; using TorchRemote.Plugin.Managers;
@@ -18,6 +19,7 @@ internal static class Statics
public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web); public static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
public static SettingManager SettingManager => Torch.Managers.GetManager<SettingManager>(); public static SettingManager SettingManager => Torch.Managers.GetManager<SettingManager>();
public static InstanceManager InstanceManager => Torch.Managers.GetManager<InstanceManager>(); public static InstanceManager InstanceManager => Torch.Managers.GetManager<InstanceManager>();
public static PluginManager PluginManager => Torch.Managers.GetManager<PluginManager>();
public static CommandManager? CommandManager => Torch.CurrentSession?.Managers.GetManager<CommandManager>(); public static CommandManager? CommandManager => Torch.CurrentSession?.Managers.GetManager<CommandManager>();
public static ChatModule ChatModule = null!; public static ChatModule ChatModule = null!;

View File

@@ -2,5 +2,5 @@
<PluginManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <PluginManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Name>Torch Remote</Name> <Name>Torch Remote</Name>
<Guid>284017F3-9682-4841-A544-EB04DB8CB9BA</Guid> <Guid>284017F3-9682-4841-A544-EB04DB8CB9BA</Guid>
<Version>v1.0.2</Version> <Version>v1.0.3</Version>
</PluginManifest> </PluginManifest>