From c20e468b99b72310f539039aa0bc7d4699cce67b Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Tue, 24 May 2022 15:49:43 +0700 Subject: [PATCH] initial --- .gitignore | 391 ++++++++++++++++++ LightPerms.Discord/Config.cs | 25 ++ LightPerms.Discord/DiscordManager.cs | 143 +++++++ LightPerms.Discord/FodyWeavers.xml | 3 + LightPerms.Discord/LightPerms.Discord.csproj | 93 +++++ LightPerms.Discord/LinkedUser.cs | 20 + LightPerms.Discord/LpCommands.cs | 26 ++ LightPerms.Discord/Plugin.cs | 29 ++ LightPerms.Discord/README.md | 5 + LightPerms.Discord/manifest.xml | 12 + LightPerms.Discord/setup.bat | 20 + LightPerms.TorchCommands/Commands.cs | 30 ++ LightPerms.TorchCommands/Extensions.cs | 10 + LightPerms.TorchCommands/FodyWeavers.xml | 3 + .../LightPerms.TorchCommands.csproj | 91 ++++ LightPerms.TorchCommands/PermissionPatch.cs | 157 +++++++ LightPerms.TorchCommands/Plugin.cs | 12 + LightPerms.TorchCommands/README.md | 11 + LightPerms.TorchCommands/manifest.xml | 12 + LightPerms.TorchCommands/setup.bat | 20 + LightPerms.sln | 28 ++ LightPerms/Config.cs | 8 + LightPerms/FodyWeavers.xml | 3 + LightPerms/Group.cs | 47 +++ LightPerms/LightPerms.csproj | 88 ++++ LightPerms/LpCommands.cs | 163 ++++++++ LightPerms/MultiplayerMembersManager.cs | 37 ++ LightPerms/PermissionsManager.cs | 85 ++++ LightPerms/Plugin.cs | 31 ++ LightPerms/README.md | 25 ++ LightPerms/manifest.xml | 6 + LightPerms/setup.bat | 20 + 32 files changed, 1654 insertions(+) create mode 100644 .gitignore create mode 100644 LightPerms.Discord/Config.cs create mode 100644 LightPerms.Discord/DiscordManager.cs create mode 100644 LightPerms.Discord/FodyWeavers.xml create mode 100644 LightPerms.Discord/LightPerms.Discord.csproj create mode 100644 LightPerms.Discord/LinkedUser.cs create mode 100644 LightPerms.Discord/LpCommands.cs create mode 100644 LightPerms.Discord/Plugin.cs create mode 100644 LightPerms.Discord/README.md create mode 100644 LightPerms.Discord/manifest.xml create mode 100644 LightPerms.Discord/setup.bat create mode 100644 LightPerms.TorchCommands/Commands.cs create mode 100644 LightPerms.TorchCommands/Extensions.cs create mode 100644 LightPerms.TorchCommands/FodyWeavers.xml create mode 100644 LightPerms.TorchCommands/LightPerms.TorchCommands.csproj create mode 100644 LightPerms.TorchCommands/PermissionPatch.cs create mode 100644 LightPerms.TorchCommands/Plugin.cs create mode 100644 LightPerms.TorchCommands/README.md create mode 100644 LightPerms.TorchCommands/manifest.xml create mode 100644 LightPerms.TorchCommands/setup.bat create mode 100644 LightPerms.sln create mode 100644 LightPerms/Config.cs create mode 100644 LightPerms/FodyWeavers.xml create mode 100644 LightPerms/Group.cs create mode 100644 LightPerms/LightPerms.csproj create mode 100644 LightPerms/LpCommands.cs create mode 100644 LightPerms/MultiplayerMembersManager.cs create mode 100644 LightPerms/PermissionsManager.cs create mode 100644 LightPerms/Plugin.cs create mode 100644 LightPerms/README.md create mode 100644 LightPerms/manifest.xml create mode 100644 LightPerms/setup.bat diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2cd9c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,391 @@ +## 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 +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.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 + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# 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 +# Note: 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 +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Nuget personal access tokens and Credentials +nuget.config + +# 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 +*.appx +*.appxbundle +*.appxupload + +# 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 +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# 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 +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# 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/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml + +GameBinaries +TorchBinaries diff --git a/LightPerms.Discord/Config.cs b/LightPerms.Discord/Config.cs new file mode 100644 index 0000000..1d0f2df --- /dev/null +++ b/LightPerms.Discord/Config.cs @@ -0,0 +1,25 @@ +using System.Collections.ObjectModel; +using Torch; +using Torch.Views; + +namespace LightPerms.Discord; + +public class Config : ViewModel +{ + [Display(Name = "Token", Description = "Discord bot token")] + public string Token { get; set; } = "bot-token"; + [Display(Name = "Guild Id", Description = "Id of the guild to work with")] + public ulong GuildId { get; set; } + [Display(Name = "Role Configs")] + public ObservableCollection RoleConfigs { get; set; } = new(); +} + +public class DiscordRoleConfig : ViewModel +{ + [Display(Name = "Role Id", Description = "Id of the discord role to work with")] + public ulong RoleId { get; set; } + [Display(Name = "Group Name", Description = "Name of the group to work with (read guide on LightPerms plugin page to create a new group)")] + public string GroupName { get; set; } = "role-group"; + + public override string ToString() => GroupName; +} diff --git a/LightPerms.Discord/DiscordManager.cs b/LightPerms.Discord/DiscordManager.cs new file mode 100644 index 0000000..e74e92b --- /dev/null +++ b/LightPerms.Discord/DiscordManager.cs @@ -0,0 +1,143 @@ +using Discord; +using Discord.WebSocket; +using NLog; +using NLog.Fluent; +using PetaPoco; +using Torch.API; +using Torch.Managers; +namespace LightPerms.Discord; + +public class DiscordManager : Manager +{ + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + private readonly Config _config; + [Dependency] + private readonly IPermissionsManager _permissionsManager = null!; + private readonly DiscordSocketClient _client = new(new() + { + GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMessages | GatewayIntents.GuildMembers + }); + + private IDatabase _db = null!; + + public Dictionary WaitingUsers { get; } = new(); + + public DiscordManager(ITorchBase torchInstance, Config config) : base(torchInstance) + { + _config = config; + _client.SlashCommandExecuted += ClientOnSlashCommandExecutedAsync; + _client.GuildMemberUpdated += ClientOnGuildMemberUpdatedAsync; + _client.Ready += ClientOnReady; + _client.Log += message => + { + NLog.Fluent.Log.Level(message.Severity switch + { + LogSeverity.Critical => LogLevel.Fatal, + LogSeverity.Error => LogLevel.Error, + LogSeverity.Warning => LogLevel.Warn, + LogSeverity.Info => LogLevel.Info, + LogSeverity.Debug => LogLevel.Debug, + LogSeverity.Verbose => LogLevel.Trace, + _ => throw new ArgumentOutOfRangeException() + }).LoggerName(message.Source).Exception(message.Exception).Message(message.Message).Write(); + return Task.CompletedTask; + }; + } + private async Task ClientOnReady() + { + var guild = _client.GetGuild(_config.GuildId); + + if (_config.RoleConfigs.Any(b => guild.Roles.All(c => c.Id != b.RoleId))) + throw new InvalidOperationException("Some roles are not exists in the guild"); + + await AddSlashCommandsAsync(guild); + + await guild.DownloadUsersAsync(); + await Task.WhenAll(guild.Users.Select(b => ClientOnGuildMemberUpdatedAsync(default, b))); + } + + public override void Attach() + { + base.Attach(); + _db = _permissionsManager.Db; + _db.Execute(LinkedUser.CreateTableSql); + + Task.Run(AttachAsync); + } + + private async Task AttachAsync() + { + await _client.LoginAsync(TokenType.Bot, _config.Token); + await _client.StartAsync(); + } + + private async Task ClientOnGuildMemberUpdatedAsync(Cacheable cacheable, SocketGuildUser user) + { + if (await _db.SingleOrDefaultAsync(Sql.Builder.Where("discord_id = @0", user.Id.ToString())) is not { } linkedUser || + await _db.SingleOrDefaultAsync(Sql.Builder.Where("client_id = @0", linkedUser.ClientId)) is not { } groupMember) + return; + + bool HasRole(ulong id) => user.Roles.Any(b => b.Id == id); + + foreach (var (role, group) in _config.RoleConfigs.Select(b => (b, _permissionsManager.GetGroup(b.GroupName)!))) + { + if (!HasRole(role.RoleId) && groupMember.GroupUid == group.Uid) + { + _permissionsManager.AssignGroup(linkedUser.ClientIdNumber, "player"); + Log.Info($"Removed group {role.GroupName} from {linkedUser.ClientId}"); + } + else if (HasRole(role.RoleId) && groupMember.GroupUid != group.Uid) + { + _permissionsManager.AssignGroup(linkedUser.ClientIdNumber, group.Name); + groupMember.GroupUid = group.Uid; + Log.Info($"Assigned group {role.GroupName} to {linkedUser.ClientId}"); + } + } + } + + private async Task ClientOnSlashCommandExecutedAsync(SocketSlashCommand arg) + { + if (arg.CommandName != "lp-link") + return; + + if (GetClientIdByDiscordId(arg.User.Id) is { }) + { + await arg.RespondAsync("You are already linked with lp."); + return; + } + + var username = Format.UsernameAndDiscriminator(arg.User, false); + + if (!WaitingUsers.ContainsKey(username)) + { + await arg.RespondAsync("Type `!lp link` in-game first."); + return; + } + + var clientId = WaitingUsers[username]; + WaitingUsers.Remove(username); + + await _db.InsertAsync(new LinkedUser + { + DiscordId = arg.User.Id.ToString(), + ClientId = clientId.ToString() + }); + await arg.RespondAsync("Linked successfully."); + } + + private Task AddSlashCommandsAsync(SocketGuild guild) + { + return guild.CreateApplicationCommandAsync(new SlashCommandBuilder() + .WithName("lp-link") + .WithDescription("Light perms link with game") + .Build()); + } + + public ulong? GetClientIdByDiscordId(ulong discordId) + { + if (_db.SingleOrDefault(Sql.Builder.Where("discord_id = @0", discordId.ToString())) is not { } user) + return null; + return ulong.Parse(user.ClientId); + } +} diff --git a/LightPerms.Discord/FodyWeavers.xml b/LightPerms.Discord/FodyWeavers.xml new file mode 100644 index 0000000..5a02dd9 --- /dev/null +++ b/LightPerms.Discord/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LightPerms.Discord/LightPerms.Discord.csproj b/LightPerms.Discord/LightPerms.Discord.csproj new file mode 100644 index 0000000..7d2d194 --- /dev/null +++ b/LightPerms.Discord/LightPerms.Discord.csproj @@ -0,0 +1,93 @@ + + + + net48 + enable + enable + x64 + 10 + true + $(SolutionDir)TorchBinaries\ + + + + none + + + + + $(TorchDir)NLog.dll + False + + + $(TorchDir)DedicatedServer64\Sandbox.Common.dll + False + + + $(TorchDir)DedicatedServer64\Sandbox.Game.dll + False + + + $(TorchDir)DedicatedServer64\Sandbox.Graphics.dll + False + + + + $(TorchDir)Torch.dll + False + + + $(TorchDir)Torch.API.dll + False + + + $(TorchDir)Torch.Server.exe + False + + + $(TorchDir)DedicatedServer64\VRage.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Game.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Input.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Library.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Math.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Network.dll + False + + + + + + + + + + + + PreserveNewest + + + + + + + + + + + + diff --git a/LightPerms.Discord/LinkedUser.cs b/LightPerms.Discord/LinkedUser.cs new file mode 100644 index 0000000..3ed2323 --- /dev/null +++ b/LightPerms.Discord/LinkedUser.cs @@ -0,0 +1,20 @@ +using PetaPoco; +namespace LightPerms.Discord; + +[TableName("linked_users")] +[PrimaryKey(nameof(Uid), AutoIncrement = true)] +public class LinkedUser +{ + [Column] + public long Uid { get; set;} + + [Column] + public string DiscordId { get; set; } = "0"; + [Column] + public string ClientId { get; set; } = "0"; + + internal const string CreateTableSql = "create table if not exists linked_users (uid INTEGER PRIMARY KEY AUTOINCREMENT, discord_id TEXT NOT NULL, client_id TEXT NOT NULL);"; + + [Ignore] + public ulong ClientIdNumber => ulong.Parse(ClientId); +} diff --git a/LightPerms.Discord/LpCommands.cs b/LightPerms.Discord/LpCommands.cs new file mode 100644 index 0000000..8dd344a --- /dev/null +++ b/LightPerms.Discord/LpCommands.cs @@ -0,0 +1,26 @@ +using System.Text.RegularExpressions; +using Torch.API.Managers; +using Torch.Commands; +using Torch.Commands.Permissions; +using VRage.Game.ModAPI; +namespace LightPerms.Discord; + +[Category("lp")] +public class LpCommands : CommandModule +{ + private static readonly Regex DiscordTagRegex = new(@"^.{3,32}#[0-9]{4}$"); + + [Command("link", "Link you with your discord account. (type `/lp-link` on server discord after)")] + [Permission(MyPromoteLevel.None)] + public void Link(string discordUsername) + { + if (!DiscordTagRegex.IsMatch(discordUsername)) + { + Context.Respond("Please enter a valid discord username! (like abcde#1234)"); + return; + } + + Context.Torch.CurrentSession.Managers.GetManager().WaitingUsers[discordUsername] = Context.Player.SteamUserId; + Context.Respond("Type `/lp-link` on server discord"); + } +} diff --git a/LightPerms.Discord/Plugin.cs b/LightPerms.Discord/Plugin.cs new file mode 100644 index 0000000..ff43ec1 --- /dev/null +++ b/LightPerms.Discord/Plugin.cs @@ -0,0 +1,29 @@ +using System.IO; +using System.Windows.Controls; +using Torch; +using Torch.API; +using Torch.API.Managers; +using Torch.API.Plugins; +using Torch.API.Session; +using Torch.Views; + +namespace LightPerms.Discord; + +public class Plugin : TorchPluginBase, IWpfPlugin +{ + private Persistent _config = null!; + + public override void Init(ITorchBase torch) + { + base.Init(torch); + _config = Persistent.Load(Path.Combine(StoragePath, "LightPerms.Discord.cfg")); + + Torch.Managers.GetManager().AddFactory(s => new DiscordManager(s.Torch, _config.Data)); + } + + public UserControl GetControl() => new PropertyGrid + { + Margin = new(3), + DataContext = _config.Data + }; +} diff --git a/LightPerms.Discord/README.md b/LightPerms.Discord/README.md new file mode 100644 index 0000000..0186138 --- /dev/null +++ b/LightPerms.Discord/README.md @@ -0,0 +1,5 @@ +Add-on for [LightPerms](https://torchapi.com/plugins/view/?guid=5c3f35b3-ac9d-486f-8559-f931536c6700) plugin to automatically assign groups based on discord role + +# Setup a bot + +if you already have a bot installed with **SEDB** or **Torch Alert** you use token from it else you have to follow the [guide](https://goo.gl/5Do8LJ) \ No newline at end of file diff --git a/LightPerms.Discord/manifest.xml b/LightPerms.Discord/manifest.xml new file mode 100644 index 0000000..b648171 --- /dev/null +++ b/LightPerms.Discord/manifest.xml @@ -0,0 +1,12 @@ + + + LightPerms.Discord + d53cf5e6-27ea-491b-9579-8506d93f184b + v1.0.1 + + + 5c3f35b3-ac9d-486f-8559-f931536c6700 + v1.0.1 + + + \ No newline at end of file diff --git a/LightPerms.Discord/setup.bat b/LightPerms.Discord/setup.bat new file mode 100644 index 0000000..ed0572f --- /dev/null +++ b/LightPerms.Discord/setup.bat @@ -0,0 +1,20 @@ +:: This script creates a symlink to the game binaries to account for different installation directories on different systems. + +@echo off + +set /p path="Please enter the folder location of your Torch.Server.exe: " +cd %~dp0 +rmdir TorchBinaries > nul 2>&1 +mklink /J ..\TorchBinaries "%path%" +if errorlevel 1 goto Error +echo Done! + +echo You can now open the plugin without issue. +goto EndFinal + +:Error +echo An error occured creating the symlink. +goto EndFinal + +:EndFinal +pause diff --git a/LightPerms.TorchCommands/Commands.cs b/LightPerms.TorchCommands/Commands.cs new file mode 100644 index 0000000..ca04aa5 --- /dev/null +++ b/LightPerms.TorchCommands/Commands.cs @@ -0,0 +1,30 @@ +using System.Text; +using Torch.API.Managers; +using Torch.Commands; +using Torch.Mod; +using Torch.Mod.Messages; +namespace LightPerms.TorchCommands; + +[Category("lp")] +public class Commands : CommandModule +{ + [Command("get commands", "Get all command permissions")] + public void GetCommands() + { + var sb = new StringBuilder(); + + if (Context.Player is null) + sb.AppendLine("Available commands:"); + + foreach (var commandNode in Context.Torch.CurrentSession.Managers.GetManager().Commands.WalkTree() + .Where(b => b.IsCommand)) + { + sb.Append(" ".PadRight(4)).AppendLine(commandNode.Command.GetPermissionString()); + } + + if (Context.Player is null) + Context.Respond(sb.ToString()); + else + ModCommunication.SendMessageTo(new DialogMessage("Light Perms", "Available commands:", sb.ToString()), Context.Player.SteamUserId); + } +} diff --git a/LightPerms.TorchCommands/Extensions.cs b/LightPerms.TorchCommands/Extensions.cs new file mode 100644 index 0000000..3f32dcb --- /dev/null +++ b/LightPerms.TorchCommands/Extensions.cs @@ -0,0 +1,10 @@ +using Torch.Commands; +namespace LightPerms.TorchCommands; + +public static class Extensions +{ + public static string GetPermissionString(this Command command) + { + return $"command.{string.Join(".", command.Path.Select(b => b.ToLowerInvariant()))}"; + } +} diff --git a/LightPerms.TorchCommands/FodyWeavers.xml b/LightPerms.TorchCommands/FodyWeavers.xml new file mode 100644 index 0000000..5a02dd9 --- /dev/null +++ b/LightPerms.TorchCommands/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LightPerms.TorchCommands/LightPerms.TorchCommands.csproj b/LightPerms.TorchCommands/LightPerms.TorchCommands.csproj new file mode 100644 index 0000000..5aa3977 --- /dev/null +++ b/LightPerms.TorchCommands/LightPerms.TorchCommands.csproj @@ -0,0 +1,91 @@ + + + + net48 + enable + enable + x64 + 10 + true + $(SolutionDir)TorchBinaries\ + + + + none + + + + + $(TorchDir)NLog.dll + False + + + $(TorchDir)DedicatedServer64\Sandbox.Common.dll + False + + + $(TorchDir)DedicatedServer64\Sandbox.Game.dll + False + + + $(TorchDir)DedicatedServer64\Sandbox.Graphics.dll + False + + + + $(TorchDir)Torch.dll + False + + + $(TorchDir)Torch.API.dll + False + + + $(TorchDir)Torch.Server.exe + False + + + $(TorchDir)DedicatedServer64\VRage.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Game.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Input.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Library.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Math.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Network.dll + False + + + + + + + + + + + + + + PreserveNewest + + + + + + + + diff --git a/LightPerms.TorchCommands/PermissionPatch.cs b/LightPerms.TorchCommands/PermissionPatch.cs new file mode 100644 index 0000000..bc17311 --- /dev/null +++ b/LightPerms.TorchCommands/PermissionPatch.cs @@ -0,0 +1,157 @@ +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using Sandbox.Game.Multiplayer; +using Sandbox.Game.World; +using Torch.API; +using Torch.API.Managers; +using Torch.Commands; +using Torch.Managers; +using Torch.Managers.PatchManager; +using Torch.Managers.PatchManager.MSIL; +using Torch.Mod; +using Torch.Mod.Messages; +using Torch.Utils; +namespace LightPerms.TorchCommands; + +[PatchShim] +public static class PermissionPatch +{ + [ReflectedMethodInfo(typeof(CommandManager), nameof(CommandManager.HasPermission))] + private static readonly MethodInfo HasPermissionMethod = null!; + + [ReflectedMethodInfo(typeof(Torch.Commands.TorchCommands), nameof(Torch.Commands.TorchCommands.Help))] + private static readonly MethodInfo HelpMethod = null!; + + [ReflectedMethodInfo(typeof(Torch.Commands.TorchCommands), nameof(Torch.Commands.TorchCommands.LongHelp))] + private static readonly MethodInfo LongHelpMethod = null!; + + [ReflectedMethodInfo(typeof(PermissionPatch), nameof(Prefix))] + private static readonly MethodInfo PrefixMethod = null!; + + [ReflectedMethodInfo(typeof(PermissionPatch), nameof(PrefixHelp))] + private static readonly MethodInfo PrefixHelpMethod = null!; + + [ReflectedMethodInfo(typeof(PermissionPatch), nameof(PrefixLongHelp))] + private static readonly MethodInfo PrefixLongHelpMethod = null!; + + [ReflectedMethodInfo(typeof(PermissionPatch), nameof(Transpiler))] + private static readonly MethodInfo TranspilerMethod = null!; + + [ReflectedGetter(Name = "Torch")] + private static readonly Func TorchGetter = null!; + + public static void Patch(PatchContext context) + { + context.GetPattern(HasPermissionMethod).Prefixes.Add(PrefixMethod); + context.GetPattern(typeof(CommandManager).GetMethod(nameof(CommandManager.HandleCommand), + new []{typeof(string), typeof(ulong), typeof(bool).MakeByRefType(), typeof(bool)})).Transpilers.Add(TranspilerMethod); + + context.GetPattern(HelpMethod).Prefixes.Add(PrefixHelpMethod); + context.GetPattern(LongHelpMethod).Prefixes.Add(PrefixLongHelpMethod); + } + + private static bool PrefixHelp(CommandModule __instance) + { + var commandManager = __instance.Context.Torch.CurrentSession.Managers.GetManager(); + commandManager.Commands.GetNode(__instance.Context.Args, out var node); + + if (node is null) + { + __instance.Context.Respond( + $"Command not found. Use the {commandManager.Prefix}longhelp command for a full list of commands."); + return false; + } + + var command = node.Command; + var children = node.Subcommands.Where(e => __instance.Context.Player == null || + !e.Value.IsCommand || + !commandManager.HasPermission(__instance.Context.Player.SteamUserId, e.Value.Command)) + .Select(x => x.Key); + + var sb = new StringBuilder(); + + if (command is not null) + { + if (__instance.Context.Player is not null && !commandManager.HasPermission(__instance.Context.Player.SteamUserId, command)) + { + __instance.Context.Respond("You are not authorized to use this command."); + return false; + } + + sb.AppendLine($"Syntax: {command.SyntaxHelp}"); + sb.Append(command.HelpText); + } + + if (node.Subcommands.Count > 0) + sb.Append($"\nSubcommands: {string.Join(", ", children)}"); + + __instance.Context.Respond(sb.ToString()); + return false; + } + + private static bool PrefixLongHelp(CommandModule __instance) + { + var commandManager = __instance.Context.Torch.CurrentSession.Managers.GetManager(); + commandManager.Commands.GetNode(__instance.Context.Args, out var node); + + if (node != null) + { + var command = node.Command; + var children = node.Subcommands.Where(e => __instance.Context.Player == null || + !e.Value.IsCommand || + !commandManager.HasPermission(__instance.Context.Player.SteamUserId, e.Value.Command)); + + var sb = new StringBuilder(); + + if (command != null && (__instance.Context.Player is null || commandManager.HasPermission(__instance.Context.Player.SteamUserId, command))) + { + sb.AppendLine($"Syntax: {command.SyntaxHelp}"); + sb.Append(command.HelpText); + } + + if (node.Subcommands.Count > 0) + sb.Append($"\nSubcommands: {string.Join(", ", children)}"); + + __instance.Context.Respond(sb.ToString()); + } + else + { + var sb = new StringBuilder(); + foreach (var command in commandManager.Commands.WalkTree()) + { + if (command.IsCommand && (__instance.Context.Player is null || commandManager.HasPermission(__instance.Context.Player.SteamUserId, command.Command))) + sb.AppendLine($"{command.Command.SyntaxHelp}\n {command.Command.HelpText}"); + } + + if (!__instance.Context.SentBySelf) + { + var m = new DialogMessage("Torch Help", "Available commands:", sb.ToString()); + ModCommunication.SendMessageTo(m, __instance.Context.Player!.SteamUserId); + } + else + __instance.Context.Respond($"Available commands: {sb}"); + } + return false; + } + + private static bool Prefix(Manager __instance, ulong steamId, Command command, ref bool __result) + { + __result = steamId == Sync.MyId || + MySession.Static.IsUserAdmin(steamId) || + TorchGetter(__instance).Managers.GetManager().HasPermission(steamId, command.GetPermissionString()); + return false; + } + + private static IEnumerable Transpiler(IEnumerable ins) + { + foreach (var instruction in ins) + { + if (instruction.OpCode == OpCodes.Ldstr && + instruction.Operand is MsilOperandInline.MsilOperandString {Value: "You need to be a {0} or higher to use that command."}) + yield return instruction.InlineValue("You don't have permission to use that command."); + else + yield return instruction; + } + } +} diff --git a/LightPerms.TorchCommands/Plugin.cs b/LightPerms.TorchCommands/Plugin.cs new file mode 100644 index 0000000..f712cb6 --- /dev/null +++ b/LightPerms.TorchCommands/Plugin.cs @@ -0,0 +1,12 @@ +using System.IO; +using System.Windows.Controls; +using Torch; +using Torch.API; +using Torch.API.Plugins; +using Torch.Views; + +namespace LightPerms.TorchCommands; + +public class Plugin : TorchPluginBase +{ +} diff --git a/LightPerms.TorchCommands/README.md b/LightPerms.TorchCommands/README.md new file mode 100644 index 0000000..1daa712 --- /dev/null +++ b/LightPerms.TorchCommands/README.md @@ -0,0 +1,11 @@ +Add-on for [LightPerms](https://torchapi.com/plugins/view/?guid=5c3f35b3-ac9d-486f-8559-f931536c6700) plugin to have better control over commands permissions. + +### Usage + +All existing torch commands are indexed as `command.something`, spaces are replaced by `.` + +`!help` and `!longhelp` commands are modified to reflect permission requirements. + +For example if you want give to players ability to use `!fixship` command, you need to invoke this command `!lp add perm player command.fixship` + +Wildcards are also supported, for example command to give to admins ability to use all lp commands (you need to create group before using that command) `!lp add perm admin command.lp.*` \ No newline at end of file diff --git a/LightPerms.TorchCommands/manifest.xml b/LightPerms.TorchCommands/manifest.xml new file mode 100644 index 0000000..da053dc --- /dev/null +++ b/LightPerms.TorchCommands/manifest.xml @@ -0,0 +1,12 @@ + + + LightPerms.TorchCommands + 0cf57780-f235-4240-99cd-ebd7463e673a + v1.0.0 + + + 5c3f35b3-ac9d-486f-8559-f931536c6700 + v1.0.1 + + + \ No newline at end of file diff --git a/LightPerms.TorchCommands/setup.bat b/LightPerms.TorchCommands/setup.bat new file mode 100644 index 0000000..ed0572f --- /dev/null +++ b/LightPerms.TorchCommands/setup.bat @@ -0,0 +1,20 @@ +:: This script creates a symlink to the game binaries to account for different installation directories on different systems. + +@echo off + +set /p path="Please enter the folder location of your Torch.Server.exe: " +cd %~dp0 +rmdir TorchBinaries > nul 2>&1 +mklink /J ..\TorchBinaries "%path%" +if errorlevel 1 goto Error +echo Done! + +echo You can now open the plugin without issue. +goto EndFinal + +:Error +echo An error occured creating the symlink. +goto EndFinal + +:EndFinal +pause diff --git a/LightPerms.sln b/LightPerms.sln new file mode 100644 index 0000000..c6b75ee --- /dev/null +++ b/LightPerms.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightPerms", "LightPerms\LightPerms.csproj", "{3963D8F4-CCB6-4305-8FEC-A19597404A19}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightPerms.Discord", "LightPerms.Discord\LightPerms.Discord.csproj", "{B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightPerms.TorchCommands", "LightPerms.TorchCommands\LightPerms.TorchCommands.csproj", "{8F9D910F-FFE6-4010-921F-5872ACF638BB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3963D8F4-CCB6-4305-8FEC-A19597404A19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3963D8F4-CCB6-4305-8FEC-A19597404A19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3963D8F4-CCB6-4305-8FEC-A19597404A19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3963D8F4-CCB6-4305-8FEC-A19597404A19}.Release|Any CPU.Build.0 = Release|Any CPU + {B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1A35416-6CFB-4AE7-A2F2-818E8F7A8C13}.Release|Any CPU.Build.0 = Release|Any CPU + {8F9D910F-FFE6-4010-921F-5872ACF638BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F9D910F-FFE6-4010-921F-5872ACF638BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F9D910F-FFE6-4010-921F-5872ACF638BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F9D910F-FFE6-4010-921F-5872ACF638BB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/LightPerms/Config.cs b/LightPerms/Config.cs new file mode 100644 index 0000000..818a2fa --- /dev/null +++ b/LightPerms/Config.cs @@ -0,0 +1,8 @@ +using Torch; + +namespace LightPerms; + +public class Config : ViewModel +{ + +} diff --git a/LightPerms/FodyWeavers.xml b/LightPerms/FodyWeavers.xml new file mode 100644 index 0000000..5a02dd9 --- /dev/null +++ b/LightPerms/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/LightPerms/Group.cs b/LightPerms/Group.cs new file mode 100644 index 0000000..988127f --- /dev/null +++ b/LightPerms/Group.cs @@ -0,0 +1,47 @@ +using PetaPoco; +namespace LightPerms; + +[TableName("groups")] +[PrimaryKey(nameof(Uid), AutoIncrement = true)] +public class Group +{ + [Column] + public long Uid { get; set; } + [Column] + public string Name { get; set; } = "group"; + + internal const string CreateTableSql = "create table if not exists groups (uid INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL);"; +} + +[TableName("permissions")] +[PrimaryKey(nameof(Uid), AutoIncrement = true)] +public class Permission +{ + [Column] + public long Uid { get; set; } + [Column] + public string Value { get; set; } = "*"; + [Column] + public long GroupUid { get; set; } + + internal const string CreateTableSql = "create table if not exists permissions (uid INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL, group_uid INTEGER, FOREIGN KEY(group_uid) REFERENCES groups(uid));"; +} + +[TableName("group_members")] +[PrimaryKey(nameof(Uid), AutoIncrement = true)] +public class GroupMember +{ + [Column] + public long Uid { get; set; } + + [Column] + public string Name { get; set; } = "no name"; + + [Column] + public string ClientId { get; set; } = "0"; + + [Column] + public long GroupUid { get; set; } + + internal const string CreateTableSql = "create table if not exists group_members (uid INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, client_id INTEGER, group_uid INTEGER, FOREIGN KEY(group_uid) REFERENCES groups(uid));"; +} diff --git a/LightPerms/LightPerms.csproj b/LightPerms/LightPerms.csproj new file mode 100644 index 0000000..d5ed674 --- /dev/null +++ b/LightPerms/LightPerms.csproj @@ -0,0 +1,88 @@ + + + + net48 + enable + enable + x64 + 10 + true + $(SolutionDir)TorchBinaries\ + + + + none + + + + + $(TorchDir)NLog.dll + False + + + $(TorchDir)DedicatedServer64\Sandbox.Common.dll + False + + + $(TorchDir)DedicatedServer64\Sandbox.Game.dll + False + + + $(TorchDir)DedicatedServer64\Sandbox.Graphics.dll + False + + + + $(TorchDir)Torch.dll + False + + + $(TorchDir)Torch.API.dll + False + + + $(TorchDir)Torch.Server.exe + False + + + $(TorchDir)DedicatedServer64\VRage.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Game.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Input.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Library.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Math.dll + False + + + $(TorchDir)DedicatedServer64\VRage.Network.dll + False + + + + + + + + + + + PreserveNewest + + + + + + + + diff --git a/LightPerms/LpCommands.cs b/LightPerms/LpCommands.cs new file mode 100644 index 0000000..b4e6519 --- /dev/null +++ b/LightPerms/LpCommands.cs @@ -0,0 +1,163 @@ +using PetaPoco; +using Torch.API.Managers; +using Torch.Commands; +using Torch.Commands.Permissions; +using VRage.Game.ModAPI; +namespace LightPerms; + +[Category("lp")] +public class LpCommands : CommandModule +{ + private IPermissionsManager Pm => Context.Torch.Managers.GetManager(); + +#if DEBUG + [Command("gen")] + public void GenTest() + { + var db = Pm.Db; + + db.Execute("delete from groups"); + db.Execute("delete from group_members"); + db.Execute("delete from permissions"); + + var playerGroup = (long)db.Insert(new Group {Name = "player"}); + db.Insert(new Permission {Value = "test.permission.player", GroupUid = playerGroup}); + + var adminGroup = (long)db.Insert(new Group {Name = "admin"}); + db.Insert(new Permission {Value = "test.permission.admin", GroupUid = adminGroup}); + + db.Insert(new GroupMember {Name = Context.Player.DisplayName, ClientId = Context.Player.SteamUserId.ToString(), GroupUid = playerGroup}); + } +#endif + + [Command("has perm")] + [Permission(MyPromoteLevel.Admin)] + public void HasPermission(string perm, ulong clientId = 0) + { + clientId = Context.Player?.SteamUserId ?? clientId; + + if (clientId == 0) + { + Context.Respond("Specify valid client id."); + return; + } + + Context.Respond(Pm.HasPermission(clientId, perm) ? "Yes" : "No"); + } + + [Command("get groups")] + [Permission(MyPromoteLevel.Admin)] + public void GetGroups() + { + Context.Respond($"Available groups:\n {string.Join("\n ", Pm.Db.Query().Select(b => $"{b.Name} - id {b.Uid}"))}"); + } + + [Command("get perms")] + [Permission(MyPromoteLevel.Admin)] + public void GetPerms(string groupName) + { + if (Pm.Db.SingleOrDefault(Sql.Builder.Where("name = @0", groupName)) is not { } group) + { + Context.Respond("No group found with given name"); + return; + } + + Context.Respond($"Available perms:\n {string.Join("\n ", Pm.Db.Query(Sql.Builder.Where("group_uid = @0", group.Uid)).Select(b => b.Value).OrderBy(b => b))}"); + } + + [Command("add group")] + [Permission(MyPromoteLevel.Admin)] + public void AddGroup(string groupName) + { + if (Pm.Db.Exists("name = @0", groupName)) + { + Context.Respond("Group already exists"); + return; + } + + Pm.Db.Insert(new Group + { + Name = groupName + }); + Context.Respond("Added"); + } + + [Command("add perm")] + [Permission(MyPromoteLevel.Admin)] + public void AddPerm(string groupName, string permission) + { + if (Pm.Db.SingleOrDefault(Sql.Builder.Where("name = @0", groupName)) is not { } group) + { + Context.Respond("No group found with given name"); + return; + } + + if (Pm.Db.Exists("group_uid = @0 and value like @1", group.Uid, permission.Replace('*', '%'))) + { + Context.Respond("Permission already exists"); + return; + } + + Pm.Db.Insert(new Permission + { + Value = permission, + GroupUid = group.Uid + }); + Context.Respond("Added"); + } + + [Command("del group")] + [Permission(MyPromoteLevel.Admin)] + public void DelGroup(string groupName) + { + if (Pm.Db.Delete(Sql.Builder.Where("name = @0", groupName)) != 1) + { + Context.Respond("Group not exists"); + return; + } + Context.Respond("Deleted"); + } + + [Command("del perm")] + [Permission(MyPromoteLevel.Admin)] + public void DelPerm(string groupName, string permission) + { + if (Pm.Db.SingleOrDefault(Sql.Builder.Where("name = @0", groupName)) is not { } group) + { + Context.Respond("No group found with given name"); + return; + } + + var count = Pm.Db.Delete(Sql.Builder.Where("group_uid = @0 and value like @1", group.Uid, permission.Replace('*', '%'))); + if (count > 0) + { + Context.Respond($"Deleted {count} permissions"); + return; + } + + Context.Respond("No permissions found"); + } + + [Command("assign group")] + [Permission(MyPromoteLevel.Admin)] + public void AssignGroup(string groupName, ulong clientId = 0) + { + if (clientId == 0) + clientId = Context.Player.SteamUserId; + + if (Pm.Db.SingleOrDefault(Sql.Builder.Where("name = @0", groupName)) is not { } group) + { + Context.Respond("No group found with given name"); + return; + } + + if (Pm.Db.Exists("group_uid = @0 and client_id = @1", group.Uid, clientId)) + { + Context.Respond("User already assigned to this group"); + return; + } + + Pm.AssignGroup(clientId, groupName); + Context.Respond("User assigned to the group"); + } +} diff --git a/LightPerms/MultiplayerMembersManager.cs b/LightPerms/MultiplayerMembersManager.cs new file mode 100644 index 0000000..2eb631b --- /dev/null +++ b/LightPerms/MultiplayerMembersManager.cs @@ -0,0 +1,37 @@ +using Torch.API; +using Torch.API.Managers; +using Torch.Managers; +namespace LightPerms; + +public class MultiplayerMembersManager : Manager +{ + [Dependency] + private readonly IPermissionsManager _permissionsManager = null!; + [Dependency] + private readonly IMultiplayerManagerServer _multiplayerManager = null!; + + public MultiplayerMembersManager(ITorchBase torchInstance) : base(torchInstance) + { + } + + public override void Attach() + { + base.Attach(); + _multiplayerManager.PlayerJoined += MultiplayerManagerOnPlayerJoined; + } + + private void MultiplayerManagerOnPlayerJoined(IPlayer player) + { + if (_permissionsManager.Db.Exists("client_id = @0", player.SteamId)) + return; + + var groupMember = new GroupMember + { + Name = player.Name, + ClientId = player.SteamId.ToString(), + GroupUid = 0 + }; + + _permissionsManager.Db.Insert(groupMember); + } +} diff --git a/LightPerms/PermissionsManager.cs b/LightPerms/PermissionsManager.cs new file mode 100644 index 0000000..463ceef --- /dev/null +++ b/LightPerms/PermissionsManager.cs @@ -0,0 +1,85 @@ +using heh; +using NLog; +using PetaPoco; +using Sandbox.Game.Multiplayer; +using Torch.API; +using Torch.API.Managers; +using Torch.Managers; +namespace LightPerms; + +public interface IPermissionsManager : IManager +{ + bool HasPermission(ulong clientId, string permission); + + void AssignGroup(ulong clientId, string groupName); + + Group? GetGroup(string groupName); + + IDatabase Db { get; } +} + +public class PermissionsManager : Manager, IPermissionsManager +{ + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + private static readonly char[] InvalidPermissionChars = {'*', '?'}; + + [Dependency] + private readonly IDbManager _dbManager = null!; + + public PermissionsManager(ITorchBase torchInstance) : base(torchInstance) + { + } + + public override void Attach() + { + base.Attach(); + Db = _dbManager.Create("light_perms"); + Db.Execute(Group.CreateTableSql); + Db.Execute(Permission.CreateTableSql); + Db.Execute(GroupMember.CreateTableSql); + + if (GetGroup("player") is null) + Db.Insert(new Group + { + Name = "player" + }); + } + + public bool HasPermission(ulong clientId, string permission) + { + if (permission.IndexOfAny(InvalidPermissionChars) > -1) + throw new InvalidOperationException("Permission should not contain any invalid characters"); + + var member = Db.SingleOrDefault(Sql.Builder.Where("client_id = @0", clientId)); + var result = member is not null && Db.Exists("group_uid = @0 and @1 like replace(replace(value,'*','%'),'?','_')", member.GroupUid, permission); + + if (!result) + Log.Info("User ({0}) has no permission '{1}'", clientId, permission); + + return result; + } + public void AssignGroup(ulong clientId, string groupName) + { + if (Sync.Players.TryGetPlayerIdentity(new(clientId)) is not { } identity) + throw new InvalidOperationException($"Invalid client id {clientId}"); + + if (Db.SingleOrDefault(Sql.Builder.Where("name = @0", groupName)) is not { } group) + throw new InvalidOperationException($"Invalid group name {groupName}"); + + if (Db.SingleOrDefault(Sql.Builder.Where("client_id = @0", clientId)) is { } groupMember) + Db.Delete(groupMember); + + Db.Insert(new GroupMember + { + Name = identity.DisplayName, + ClientId = clientId.ToString(), + GroupUid = group.Uid + }); + } + public Group? GetGroup(string groupName) + { + return Db.SingleOrDefault(Sql.Builder.Where("name = @0", groupName)); + } + + public IDatabase Db { get; private set; } = null!; +} diff --git a/LightPerms/Plugin.cs b/LightPerms/Plugin.cs new file mode 100644 index 0000000..e064811 --- /dev/null +++ b/LightPerms/Plugin.cs @@ -0,0 +1,31 @@ +using System.IO; +using System.Windows.Controls; +using heh; +using Torch; +using Torch.API; +using Torch.API.Managers; +using Torch.API.Plugins; +using Torch.API.Session; +using Torch.Views; + +namespace LightPerms; + +public class Plugin : TorchPluginBase, IWpfPlugin +{ + private Persistent _config = null!; + + public override void Init(ITorchBase torch) + { + base.Init(torch); + _config = Persistent.Load(Path.Combine(StoragePath, "LightPerms.cfg")); + Torch.Managers.AddManager(DbManager.Static); + Torch.Managers.AddManager(new PermissionsManager(Torch)); + Torch.Managers.GetManager().AddFactory(s => new MultiplayerMembersManager(s.Torch)); + } + + public UserControl GetControl() => new PropertyGrid + { + Margin = new(3), + DataContext = _config.Data + }; +} diff --git a/LightPerms/README.md b/LightPerms/README.md new file mode 100644 index 0000000..8a15c97 --- /dev/null +++ b/LightPerms/README.md @@ -0,0 +1,25 @@ +Light plugin to add permission system (like minecraft's PermissionEx or LuckPerms) to your server. + + +In this plugins all permissions can be wildcards and belongs to groups + +By default plugin creates and assigns `player` group to all players automatically (currently unchangeable) + +### Commands + ++ `!lp get groups` - Returns list of all groups ++ `!lp add group ` - Creates a new group ++ `!lp del groups ` - Deletes the group ++ `!lp get perms ` - Returns list of permissions in the group ++ `!lp add perm ` - Adds a new permission to the group ++ `!lp del perm ` - Deletes a permission from the group ++ `!lp has perm ` - Returns if player has a permission (client id can be removed to use the sender id) ++ `!lp assign group ` - Assigns a group to the player (client id can be removed to use the sender id) + +### Groups based on discord roles + +To automatically assign a group to players if they have a specific role in discord server use [LightPerms.Discord](https://torchapi.com/plugins/view/?guid=d53cf5e6-27ea-491b-9579-8506d93f184b) plugin + +### Support in plugins + ++ [Kits](https://torchapi.com/plugins/view/?guid=d095391d-b5ec-43a9-8ba4-6c4909227e6e) \ No newline at end of file diff --git a/LightPerms/manifest.xml b/LightPerms/manifest.xml new file mode 100644 index 0000000..2aa6f27 --- /dev/null +++ b/LightPerms/manifest.xml @@ -0,0 +1,6 @@ + + + LightPerms + 5c3f35b3-ac9d-486f-8559-f931536c6700 + v1.0.1 + \ No newline at end of file diff --git a/LightPerms/setup.bat b/LightPerms/setup.bat new file mode 100644 index 0000000..ed0572f --- /dev/null +++ b/LightPerms/setup.bat @@ -0,0 +1,20 @@ +:: This script creates a symlink to the game binaries to account for different installation directories on different systems. + +@echo off + +set /p path="Please enter the folder location of your Torch.Server.exe: " +cd %~dp0 +rmdir TorchBinaries > nul 2>&1 +mklink /J ..\TorchBinaries "%path%" +if errorlevel 1 goto Error +echo Done! + +echo You can now open the plugin without issue. +goto EndFinal + +:Error +echo An error occured creating the symlink. +goto EndFinal + +:EndFinal +pause