From 6d2a5a63171e1084c5ccb9634aecea0931638827 Mon Sep 17 00:00:00 2001 From: Luck Date: Thu, 8 Jun 2023 21:28:15 +0100 Subject: [PATCH 01/23] Fix issues with postgres storage backend (#3647) --- .../me/lucko/luckperms/common/dependencies/Dependency.java | 4 ++-- standalone/app/build.gradle | 1 + standalone/docker/readme.md | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 standalone/docker/readme.md diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java index c66dcbb6a..d58b5f75e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java @@ -56,8 +56,8 @@ public enum Dependency { JAR_RELOCATOR( "me.lucko", "jar-relocator", - "1.4", - "1RsiF3BiVztjlfTA+svDCuoDSGFuSpTZYHvUK8yBx8I=" + "1.7", + "b30RhOF6kHiHl+O5suNLh/+eAr1iOFEFLXhwkHHDu4I=" ), ADVENTURE( diff --git a/standalone/app/build.gradle b/standalone/app/build.gradle index 696641afb..cd9dd95f9 100644 --- a/standalone/app/build.gradle +++ b/standalone/app/build.gradle @@ -16,6 +16,7 @@ dependencies { api 'com.google.code.gson:gson:2.9.0' api 'com.google.guava:guava:31.1-jre' + api 'io.netty:netty-all:4.1.93.Final' api('net.kyori:adventure-api:4.11.0') { exclude(module: 'adventure-bom') diff --git a/standalone/docker/readme.md b/standalone/docker/readme.md new file mode 100644 index 000000000..55a149f33 --- /dev/null +++ b/standalone/docker/readme.md @@ -0,0 +1,5 @@ +### Docker Build instructions + +1. Compile with Gradle +2. `cd standalone/loader/build/libs` +3. `docker build . -t luckperms:luckperms -f ../../../docker/Dockerfile` From 8048427b383a42066c9cc7e31c12376592bb4b56 Mon Sep 17 00:00:00 2001 From: Luck Date: Thu, 8 Jun 2023 22:01:21 +0100 Subject: [PATCH 02/23] Tag docker images with LP version --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b84bce2f7..9a2083f48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,6 +98,10 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Determine the LuckPerms version + run: | + echo "luckperms_version=$(git describe --tags | awk -F "-" '{print $1 "." $2}')" >> "$GITHUB_ENV" + - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v3 @@ -105,6 +109,12 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} flavor: | latest=${{ github.ref == 'refs/heads/master' }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=raw,enable=${{ github.ref == 'refs/heads/master' }},value=${{ env.luckperms_version }} - name: Build and push Docker image uses: docker/build-push-action@v2 From da08be0b2074c6e0b8411e7f842dbaba0dafbf91 Mon Sep 17 00:00:00 2001 From: Luck Date: Thu, 8 Jun 2023 22:16:16 +0100 Subject: [PATCH 03/23] Fix github action to fetch repo with tags --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a2083f48..2391ba1f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Retrieve saved standalone jar artifact uses: actions/download-artifact@v3 @@ -81,10 +83,6 @@ jobs: name: standalone-binary path: standalone/docker/ - - name: Display structure of downloaded files - run: ls -R - working-directory: standalone/docker/ - - name: Set up QEMU uses: docker/setup-qemu-action@v1 From d859085056010bf1830c6a7f1b8234a393486d71 Mon Sep 17 00:00:00 2001 From: Luck Date: Tue, 30 May 2023 12:57:15 +0200 Subject: [PATCH 04/23] Update Fabric/Forge to Minecraft 1.20 Co-authored-by: Aurelien --- bukkit-legacy/build.gradle | 2 +- bukkit/build.gradle | 2 +- bungee/build.gradle | 2 +- fabric/build.gradle | 16 +++++----- .../luckperms/fabric/FabricSenderFactory.java | 6 ++-- .../context/FabricPlayerCalculator.java | 2 +- .../fabric/mixin/ServerPlayerEntityMixin.java | 4 +-- forge/build.gradle | 29 +++--------------- forge/forge-api/build.gradle | 18 ++--------- forge/gradle.properties | 4 +-- forge/loader/build.gradle | 25 +++------------ .../luckperms/forge/ForgeSenderFactory.java | 2 +- .../forge/context/ForgePlayerCalculator.java | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 25 ++++++++++----- gradlew.bat | 15 +++++---- nukkit/build.gradle | 2 +- settings.gradle | 7 +++-- sponge/build.gradle | 2 +- standalone/build.gradle | 2 +- velocity/build.gradle | 2 +- 22 files changed, 70 insertions(+), 102 deletions(-) diff --git a/bukkit-legacy/build.gradle b/bukkit-legacy/build.gradle index 772630d6d..6c9853388 100644 --- a/bukkit-legacy/build.gradle +++ b/bukkit-legacy/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' } dependencies { diff --git a/bukkit/build.gradle b/bukkit/build.gradle index f7cae794f..d4e636c02 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' } repositories { diff --git a/bungee/build.gradle b/bungee/build.gradle index 00ffa4171..188043578 100644 --- a/bungee/build.gradle +++ b/bungee/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' } dependencies { diff --git a/fabric/build.gradle b/fabric/build.gradle index 58a298b2c..aa43338d8 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -1,8 +1,8 @@ import net.fabricmc.loom.task.RemapJarTask plugins { - id 'com.github.johnrengelman.shadow' version '7.1.2' - id 'fabric-loom' version '1.1-SNAPSHOT' + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'fabric-loom' version '1.2-SNAPSHOT' } archivesBaseName = 'luckperms' @@ -14,9 +14,9 @@ repositories { dependencies { // https://modmuss50.me/fabric.html - minecraft 'com.mojang:minecraft:1.19.4' - mappings 'net.fabricmc:yarn:1.19.4+build.1:v2' - modImplementation 'net.fabricmc:fabric-loader:0.14.17' + minecraft 'com.mojang:minecraft:1.20' + mappings 'net.fabricmc:yarn:1.20+build.1:v2' + modImplementation 'net.fabricmc:fabric-loader:0.14.21' Set apiModules = [ 'fabric-api-base', @@ -26,7 +26,7 @@ dependencies { ] apiModules.forEach { - modImplementation(fabricApi.module(it, '0.76.0+1.19.4')) + modImplementation(fabricApi.module(it, '0.83.0+1.20')) } include(modImplementation('me.lucko:fabric-permissions-api:0.2-SNAPSHOT')) @@ -75,8 +75,8 @@ shadowJar { task remappedShadowJar(type: RemapJarTask) { dependsOn tasks.shadowJar - input.set(tasks.shadowJar.archiveFile) - addNestedDependencies.set(true) + input = tasks.shadowJar.archiveFile + addNestedDependencies = true archiveFileName = "LuckPerms-Fabric-${project.ext.fullVersion}.jar" } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java index d6a0713a9..0da93bef0 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java @@ -72,11 +72,13 @@ protected String getName(ServerCommandSource commandSource) { @Override protected void sendMessage(ServerCommandSource sender, Component message) { - Locale locale = null; + final Locale locale; if (sender.getEntity() instanceof ServerPlayerEntity) { locale = ((MixinUser) sender.getEntity()).getCachedLocale(); + } else { + locale = null; } - sender.sendFeedback(toNativeText(TranslationManager.render(message, locale)), false); + sender.sendFeedback(() -> toNativeText(TranslationManager.render(message, locale)), false); } @Override diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricPlayerCalculator.java b/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricPlayerCalculator.java index 87cfb368f..4313f42f1 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricPlayerCalculator.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricPlayerCalculator.java @@ -79,7 +79,7 @@ public void calculate(@NonNull ServerPlayerEntity target, @NonNull ContextConsum } // TODO: figure out dimension type context too - ServerWorld world = target.getWorld(); + ServerWorld world = target.getServerWorld(); if (this.world) { this.plugin.getConfiguration().get(ConfigKeys.WORLD_REWRITES).rewriteAndSubmit(getContextKey(world.getRegistryKey().getValue()), consumer); } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java index fc99d810e..c7545660a 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java @@ -69,7 +69,7 @@ public abstract class ServerPlayerEntityMixin implements MixinUser { private Locale luckperms$locale; // Used by PlayerChangeWorldCallback hook below. - @Shadow public abstract ServerWorld getWorld(); + @Shadow public abstract ServerWorld getServerWorld(); @Override public User getLuckPermsUser() { @@ -183,6 +183,6 @@ private void luckperms_setClientSettings(ClientSettingsC2SPacket information, Ca @Inject(at = @At("TAIL"), method = "worldChanged") private void luckperms_onChangeDimension(ServerWorld targetWorld, CallbackInfo ci) { - PlayerChangeWorldCallback.EVENT.invoker().onChangeWorld(this.getWorld(), targetWorld, (ServerPlayerEntity) (Object) this); + PlayerChangeWorldCallback.EVENT.invoker().onChangeWorld(this.getServerWorld(), targetWorld, (ServerPlayerEntity) (Object) this); } } diff --git a/forge/build.gradle b/forge/build.gradle index b7e674267..5fde91d68 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,20 +1,9 @@ -buildscript { - repositories { - maven { url 'https://plugins.gradle.org/m2' } - maven { url 'https://maven.minecraftforge.net/' } - } - - dependencies { - classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2' - classpath 'net.kyori:blossom:1.3.0' - classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+' - } +plugins { + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'net.kyori.blossom' version '1.3.0' + id 'net.minecraftforge.gradle' version '[6.0,6.2)' } -apply plugin: 'com.github.johnrengelman.shadow' -apply plugin: 'net.kyori.blossom' -apply plugin: 'net.minecraftforge.gradle' - sourceCompatibility = 1.8 targetCompatibility = 17 @@ -27,12 +16,6 @@ minecraft { mappings channel: 'official', version: minecraftVersion } -configurations { -} - -repositories { -} - dependencies { minecraft "net.minecraftforge:forge:${minecraftVersion}-${forgeVersion}" implementation project(':common') @@ -40,10 +23,6 @@ dependencies { compileOnly project(':forge:forge-api') } -reobf { - shadowJar {} -} - shadowJar { archiveFileName = "luckperms-forge.jarinjar" diff --git a/forge/forge-api/build.gradle b/forge/forge-api/build.gradle index 8edccffbe..71df388c5 100644 --- a/forge/forge-api/build.gradle +++ b/forge/forge-api/build.gradle @@ -1,15 +1,7 @@ -buildscript { - repositories { - maven { url 'https://maven.minecraftforge.net/' } - } - - dependencies { - classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+' - } +plugins { + id 'net.minecraftforge.gradle' version '[6.0,6.2)' } -apply plugin: 'net.minecraftforge.gradle' - sourceCompatibility = 1.8 targetCompatibility = 17 @@ -17,12 +9,6 @@ minecraft { mappings channel: 'official', version: minecraftVersion } -configurations { -} - -repositories { -} - dependencies { minecraft "net.minecraftforge:forge:${minecraftVersion}-${forgeVersion}" implementation project(':api') diff --git a/forge/gradle.properties b/forge/gradle.properties index f081a1dcb..c7eb686d7 100644 --- a/forge/gradle.properties +++ b/forge/gradle.properties @@ -1,2 +1,2 @@ -minecraftVersion=1.19.4 -forgeVersion=45.0.1 \ No newline at end of file +minecraftVersion=1.20 +forgeVersion=46.0.1 \ No newline at end of file diff --git a/forge/loader/build.gradle b/forge/loader/build.gradle index 3eca8f201..f088225dc 100644 --- a/forge/loader/build.gradle +++ b/forge/loader/build.gradle @@ -1,19 +1,9 @@ -buildscript { - repositories { - maven { url 'https://plugins.gradle.org/m2' } - maven { url 'https://maven.minecraftforge.net/' } - } - - dependencies { - classpath 'gradle.plugin.com.github.johnrengelman:shadow:7.1.2' - classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+' - } +plugins { + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'java-library' + id 'net.minecraftforge.gradle' version '[6.0,6.2)' } -apply plugin: 'com.github.johnrengelman.shadow' -apply plugin: 'java-library' -apply plugin: 'net.minecraftforge.gradle' - sourceCompatibility = 1.8 targetCompatibility = 17 @@ -21,9 +11,6 @@ minecraft { mappings channel: 'official', version: minecraftVersion } -repositories { -} - dependencies { minecraft "net.minecraftforge:forge:${minecraftVersion}-${forgeVersion}" implementation project(':api') @@ -56,10 +43,6 @@ processResources { } } -reobf { - shadowJar {} -} - shadowJar { archiveFileName = "LuckPerms-Forge-${project.ext.fullVersion}.jar" diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java index 8a8f63a2e..911e17d45 100644 --- a/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java @@ -77,7 +77,7 @@ protected void sendMessage(CommandSourceStack sender, Component message) { locale = null; } - sender.sendSuccess(toNativeText(TranslationManager.render(message, locale)), false); + sender.sendSuccess(() -> toNativeText(TranslationManager.render(message, locale)), false); } @Override diff --git a/forge/src/main/java/me/lucko/luckperms/forge/context/ForgePlayerCalculator.java b/forge/src/main/java/me/lucko/luckperms/forge/context/ForgePlayerCalculator.java index 6f37ec65d..bb5716018 100644 --- a/forge/src/main/java/me/lucko/luckperms/forge/context/ForgePlayerCalculator.java +++ b/forge/src/main/java/me/lucko/luckperms/forge/context/ForgePlayerCalculator.java @@ -68,7 +68,7 @@ public ForgePlayerCalculator(LPForgePlugin plugin, Set disabled) { @Override public void calculate(@NonNull ServerPlayer target, @NonNull ContextConsumer consumer) { - ServerLevel level = target.getLevel(); + ServerLevel level = target.serverLevel(); if (this.dimensionType) { consumer.accept(DefaultContextKeys.DIMENSION_TYPE_KEY, getContextKey(level.dimension().location())); } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 40133 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/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 107acd32c..93e3f59f1 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/nukkit/build.gradle b/nukkit/build.gradle index b97fa9106..da926e01c 100644 --- a/nukkit/build.gradle +++ b/nukkit/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' } repositories { diff --git a/settings.gradle b/settings.gradle index b03ef276d..2f64daeb3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,10 +1,13 @@ -// Fabric Needs this pluginManagement { repositories { - jcenter() maven { + name = 'Fabric' url 'https://maven.fabricmc.net/' } + maven { + name = 'Forge' + url = 'https://maven.minecraftforge.net/' + } gradlePluginPortal() } } diff --git a/sponge/build.gradle b/sponge/build.gradle index 0c19a9008..c1b895243 100644 --- a/sponge/build.gradle +++ b/sponge/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' } repositories { diff --git a/standalone/build.gradle b/standalone/build.gradle index 7962117da..d5269e6fb 100644 --- a/standalone/build.gradle +++ b/standalone/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' } sourceCompatibility = 17 diff --git a/velocity/build.gradle b/velocity/build.gradle index c43a6f256..8b3ed2938 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -1,6 +1,6 @@ plugins { id 'net.kyori.blossom' version '1.3.0' - id 'com.github.johnrengelman.shadow' version '7.1.2' + id 'com.github.johnrengelman.shadow' version '8.1.1' } repositories { From 2e5a1d79a4b5e019645d24d703d149e41d1f3726 Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 11 Jun 2023 14:36:00 +0100 Subject: [PATCH 05/23] Switch back to official Postgres JDBC driver --- bukkit-legacy/build.gradle | 2 +- bukkit/build.gradle | 2 +- bungee/build.gradle | 2 +- common/build.gradle | 2 +- .../common/dependencies/Dependency.java | 17 ++--- .../dependencies/DependencyRegistry.java | 2 +- .../messaging/postgres/PostgresMessenger.java | 64 ++++++++++++------- .../hikari/PostgresConnectionFactory.java | 10 +-- fabric/build.gradle | 2 +- forge/build.gradle | 2 +- nukkit/build.gradle | 2 +- sponge/build.gradle | 2 +- standalone/build.gradle | 4 +- velocity/build.gradle | 2 +- 14 files changed, 59 insertions(+), 56 deletions(-) diff --git a/bukkit-legacy/build.gradle b/bukkit-legacy/build.gradle index 6c9853388..3e21ee1a6 100644 --- a/bukkit-legacy/build.gradle +++ b/bukkit-legacy/build.gradle @@ -29,7 +29,7 @@ shadowJar { relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' - relocate 'com.impossibl', 'me.lucko.luckperms.lib.postgresql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' diff --git a/bukkit/build.gradle b/bukkit/build.gradle index d4e636c02..9569036d8 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -39,7 +39,7 @@ shadowJar { relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' - relocate 'com.impossibl', 'me.lucko.luckperms.lib.postgresql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' diff --git a/bungee/build.gradle b/bungee/build.gradle index 188043578..6ea4a7cb5 100644 --- a/bungee/build.gradle +++ b/bungee/build.gradle @@ -31,7 +31,7 @@ shadowJar { relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' - relocate 'com.impossibl', 'me.lucko.luckperms.lib.postgresql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' diff --git a/common/build.gradle b/common/build.gradle index a22461cef..5958da14a 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -78,6 +78,6 @@ dependencies { compileOnly 'io.nats:jnats:2.16.4' compileOnly 'com.rabbitmq:amqp-client:5.12.0' compileOnly 'org.mongodb:mongodb-driver-legacy:4.5.0' - compileOnly 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.9' + compileOnly 'org.postgresql:postgresql:42.6.0' compileOnly 'org.yaml:snakeyaml:1.28' } diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java index d58b5f75e..3ec7fa6b4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java @@ -153,18 +153,11 @@ public enum Dependency { Relocation.of("mysql", "com{}mysql") ), POSTGRESQL_DRIVER( - "com{}impossibl{}pgjdbc-ng", - "pgjdbc-ng", - "0.8.9", - "WEYMezl02Rot2n2ATs7NABcvL9ceQ/oLA/XPduIEaWA=", - Relocation.of("postgresql", "com{}impossibl") - ), - POSTGRESQL_DRIVER_SPY( - "com{}impossibl{}pgjdbc-ng", - "spy", - "0.8.9", - "72ZuhpMy/4EYJZuSjBjGI5NGgdWmOpwjDHW9ISnqso8=", - Relocation.of("postgresql", "com{}impossibl") + "org{}postgresql", + "postgresql", + "42.6.0", + "uBfGekDJQkn9WdTmhuMyftDT0/rkJrINoPHnVlLPxGE=", + Relocation.of("postgresql", "org{}postgresql") ), H2_DRIVER_LEGACY( "com.h2database", diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRegistry.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRegistry.java index 45b4e52a5..73b3b08e3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRegistry.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRegistry.java @@ -55,7 +55,7 @@ public class DependencyRegistry { .putAll(StorageType.MONGODB, Dependency.MONGODB_DRIVER_CORE, Dependency.MONGODB_DRIVER_LEGACY, Dependency.MONGODB_DRIVER_SYNC, Dependency.MONGODB_DRIVER_BSON) .putAll(StorageType.MARIADB, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE, Dependency.HIKARI, Dependency.MARIADB_DRIVER) .putAll(StorageType.MYSQL, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE, Dependency.HIKARI, Dependency.MYSQL_DRIVER) - .putAll(StorageType.POSTGRESQL, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE, Dependency.HIKARI, Dependency.POSTGRESQL_DRIVER, Dependency.POSTGRESQL_DRIVER_SPY) + .putAll(StorageType.POSTGRESQL, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE, Dependency.HIKARI, Dependency.POSTGRESQL_DRIVER) .putAll(StorageType.SQLITE, Dependency.SQLITE_DRIVER) .putAll(StorageType.H2, Dependency.H2_DRIVER) .build(); diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/postgres/PostgresMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/postgres/PostgresMessenger.java index 1f73fdf54..a79a9b63f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/postgres/PostgresMessenger.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/postgres/PostgresMessenger.java @@ -25,8 +25,6 @@ package me.lucko.luckperms.common.messaging.postgres; -import com.impossibl.postgres.api.jdbc.PGConnection; -import com.impossibl.postgres.api.jdbc.PGNotificationListener; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; @@ -34,12 +32,17 @@ import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.message.OutgoingMessage; import org.checkerframework.checker.nullness.qual.NonNull; +import org.postgresql.PGConnection; +import org.postgresql.PGNotification; +import org.postgresql.util.PSQLException; +import java.net.SocketException; +import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.Statement; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; /** * An implementation of {@link Messenger} using Postgres. @@ -68,7 +71,7 @@ public void init() { @Override public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { - try (PGConnection connection = this.sqlStorage.getConnectionFactory().getConnection().unwrap(PGConnection.class)) { + try (Connection connection = this.sqlStorage.getConnectionFactory().getConnection()) { try (PreparedStatement ps = connection.prepareStatement("SELECT pg_notify(?, ?)")) { ps.setString(1, CHANNEL); ps.setString(2, outgoingMessage.asEncodedString()); @@ -122,47 +125,60 @@ private boolean checkAndReopenConnection(boolean firstStartup) { } } - private class NotificationListener implements PGNotificationListener, AutoCloseable { - private final CountDownLatch latch = new CountDownLatch(1); - private final AtomicBoolean listening = new AtomicBoolean(false); + private class NotificationListener implements AutoCloseable { + private static final int RECEIVE_TIMEOUT_MILLIS = 1000; - public void listenAndBind() { - try (PGConnection connection = PostgresMessenger.this.sqlStorage.getConnectionFactory().getConnection().unwrap(PGConnection.class)) { - connection.addNotificationListener(CHANNEL, this); + private final AtomicBoolean open = new AtomicBoolean(true); + private final AtomicReference listeningThread = new AtomicReference<>(); + public void listenAndBind() { + try (Connection connection = PostgresMessenger.this.sqlStorage.getConnectionFactory().getConnection()) { try (Statement s = connection.createStatement()) { s.execute("LISTEN \"" + CHANNEL + "\""); } - this.listening.set(true); - this.latch.await(); + PGConnection pgConnection = connection.unwrap(PGConnection.class); + this.listeningThread.set(Thread.currentThread()); + + while (this.open.get()) { + PGNotification[] notifications = pgConnection.getNotifications(RECEIVE_TIMEOUT_MILLIS); + if (notifications != null) { + for (PGNotification notification : notifications) { + handleNotification(notification); + } + } + } + + } catch (PSQLException e) { + if (!(e.getCause() instanceof SocketException && e.getCause().getMessage().equals("Socket closed"))) { + e.printStackTrace(); + } } catch (Exception e) { e.printStackTrace(); } finally { - this.listening.set(false); + this.listeningThread.set(null); } } public boolean isListening() { - return this.listening.get(); + return this.listeningThread.get() != null; } - @Override - public void notification(int processId, String channelName, String payload) { - if (!CHANNEL.equals(channelName)) { + public void handleNotification(PGNotification notification) { + if (!CHANNEL.equals(notification.getName())) { return; } - PostgresMessenger.this.consumer.consumeIncomingMessageAsString(payload); - } - - @Override - public void closed() { - this.latch.countDown(); + PostgresMessenger.this.consumer.consumeIncomingMessageAsString(notification.getParameter()); } @Override public void close() { - this.latch.countDown(); + if (this.open.compareAndSet(true, false)) { + Thread thread = this.listeningThread.get(); + if (thread != null) { + thread.interrupt(); + } + } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgresConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgresConnectionFactory.java index 6fcfb8eae..ec4d7ae15 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgresConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgresConnectionFactory.java @@ -48,9 +48,9 @@ protected String defaultPort() { @Override protected void configureDatabase(HikariConfig config, String address, String port, String databaseName, String username, String password) { - config.setDataSourceClassName("com.impossibl.postgres.jdbc.PGDataSource"); + config.setDataSourceClassName("org.postgresql.ds.PGSimpleDataSource"); config.addDataSourceProperty("serverName", address); - config.addDataSourceProperty("portNumber", Integer.parseInt(port)); + config.addDataSourceProperty("portNumber", port); config.addDataSourceProperty("databaseName", databaseName); config.addDataSourceProperty("user", username); config.addDataSourceProperty("password", password); @@ -63,12 +63,6 @@ protected void overrideProperties(Map properties) { // remove the default config properties which don't exist for PostgreSQL properties.remove("useUnicode"); properties.remove("characterEncoding"); - - // socketTimeout -> networkTimeout - Object socketTimeout = properties.remove("socketTimeout"); - if (socketTimeout != null) { - properties.putIfAbsent("networkTimeout", Integer.parseInt(socketTimeout.toString())); - } } @Override diff --git a/fabric/build.gradle b/fabric/build.gradle index aa43338d8..f5b0c4e2c 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -61,7 +61,7 @@ shadowJar { relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' - relocate 'com.impossibl', 'me.lucko.luckperms.lib.postgresql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' diff --git a/forge/build.gradle b/forge/build.gradle index 5fde91d68..8904a7b77 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -39,7 +39,7 @@ shadowJar { relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' - relocate 'com.impossibl', 'me.lucko.luckperms.lib.postgresql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' diff --git a/nukkit/build.gradle b/nukkit/build.gradle index da926e01c..92c0233aa 100644 --- a/nukkit/build.gradle +++ b/nukkit/build.gradle @@ -29,7 +29,7 @@ shadowJar { relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' - relocate 'com.impossibl', 'me.lucko.luckperms.lib.postgresql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' diff --git a/sponge/build.gradle b/sponge/build.gradle index c1b895243..f618eaff3 100644 --- a/sponge/build.gradle +++ b/sponge/build.gradle @@ -42,7 +42,7 @@ shadowJar { relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' - relocate 'com.impossibl', 'me.lucko.luckperms.lib.postgresql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' diff --git a/standalone/build.gradle b/standalone/build.gradle index d5269e6fb..c44f0c862 100644 --- a/standalone/build.gradle +++ b/standalone/build.gradle @@ -32,7 +32,7 @@ dependencies { testImplementation 'redis.clients:jedis:3.5.2' testImplementation 'io.nats:jnats:2.16.4' testImplementation 'com.rabbitmq:amqp-client:5.12.0' - testImplementation 'com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.9' + testImplementation 'org.postgresql:postgresql:42.6.0' testImplementation 'com.h2database:h2:2.1.214' testImplementation 'mysql:mysql-connector-java:8.0.23' testImplementation 'org.mariadb.jdbc:mariadb-java-client:3.1.3' @@ -60,7 +60,7 @@ shadowJar { relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' - relocate 'com.impossibl', 'me.lucko.luckperms.lib.postgresql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' diff --git a/velocity/build.gradle b/velocity/build.gradle index 8b3ed2938..e255e729f 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -36,7 +36,7 @@ shadowJar { relocate 'me.lucko.commodore', 'me.lucko.luckperms.lib.commodore' relocate 'org.mariadb.jdbc', 'me.lucko.luckperms.lib.mariadb' relocate 'com.mysql', 'me.lucko.luckperms.lib.mysql' - relocate 'com.impossibl', 'me.lucko.luckperms.lib.postgresql' + relocate 'org.postgresql', 'me.lucko.luckperms.lib.postgresql' relocate 'com.zaxxer.hikari', 'me.lucko.luckperms.lib.hikari' relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' From 081a4f86516ff1b5b0655cce5ea3d8ea0a87a316 Mon Sep 17 00:00:00 2001 From: Luck Date: Wed, 14 Jun 2023 21:18:24 +0100 Subject: [PATCH 06/23] Fix Forge reobf shadowjar (#3675) --- forge/build.gradle | 4 +++ forge/loader/build.gradle | 4 +++ .../standalone/MessagingIntegrationTest.java | 30 +++++++++++++++++-- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/forge/build.gradle b/forge/build.gradle index 8904a7b77..1d79b1370 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -23,6 +23,10 @@ dependencies { compileOnly project(':forge:forge-api') } +reobf { + shadowJar {} +} + shadowJar { archiveFileName = "luckperms-forge.jarinjar" diff --git a/forge/loader/build.gradle b/forge/loader/build.gradle index f088225dc..85242c1bb 100644 --- a/forge/loader/build.gradle +++ b/forge/loader/build.gradle @@ -43,6 +43,10 @@ processResources { } } +reobf { + shadowJar {} +} + shadowJar { archiveFileName = "LuckPerms-Forge-${project.ext.fullVersion}.jar" diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java index 44645dcaa..cb3cb12fa 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java @@ -26,9 +26,13 @@ package me.lucko.luckperms.standalone; import com.google.common.collect.ImmutableMap; +import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.standalone.app.integration.HealthReporter; import me.lucko.luckperms.standalone.utils.TestPluginProvider; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.event.EventBus; +import net.luckperms.api.event.log.LogReceiveEvent; import net.luckperms.api.event.sync.PreNetworkSyncEvent; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; @@ -41,6 +45,7 @@ import java.nio.file.Path; import java.util.Map; +import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -69,14 +74,33 @@ private static void testMessaging(Map config, Path tempDirA, Pat assertNotNull(messagingServiceA); assertNotNull(messagingServiceB); - CountDownLatch latch = new CountDownLatch(1); - pluginB.app().getApi().getEventBus().subscribe(PreNetworkSyncEvent.class, e -> { + LoggedAction exampleLogEntry = LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("hello 123 hello 123") + .build(); + + // register 2 listeners on plugin B + CountDownLatch latch = new CountDownLatch(2); + EventBus eventBus = pluginB.app().getApi().getEventBus(); + eventBus.subscribe(PreNetworkSyncEvent.class, e -> { latch.countDown(); e.setCancelled(true); }); + eventBus.subscribe(LogReceiveEvent.class, e -> { + if (e.getEntry().equals(exampleLogEntry)) { + latch.countDown(); + } + }); - // send a message from plugin A to plugin B and wait for the message to be received + // send some messages from plugin A to plugin B messagingServiceA.pushUpdate(); + messagingServiceA.pushLog(exampleLogEntry); + + // wait for the messages to be sent/received assertTrue(latch.await(30, TimeUnit.SECONDS)); } } From acd8d6e5a1883080ab19350276327de5c2f9fa2e Mon Sep 17 00:00:00 2001 From: powercas_gamer Date: Wed, 14 Jun 2023 22:19:22 +0200 Subject: [PATCH 07/23] Include standalone data directory in gitignore (#3671) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1cb4ee2b9..f74dbcc7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +### LuckPerms Standalone ### +standalone/loader/data/ + ### Gradle & IntelliJ ### .gradle/ /.idea/ From 3542a6cc75a53217c971e236446e2e6a786bd3ef Mon Sep 17 00:00:00 2001 From: Drex Date: Wed, 14 Jun 2023 22:25:25 +0200 Subject: [PATCH 08/23] Improve console detection on Fabric/Forge (#3673) --- .../luckperms/fabric/FabricSenderFactory.java | 8 +++- .../mixin/ServerCommandSourceAccessor.java | 42 +++++++++++++++++++ .../src/main/resources/luckperms.mixins.json | 1 + .../luckperms/forge/ForgeSenderFactory.java | 7 +++- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerCommandSourceAccessor.java diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java index 0da93bef0..57538da83 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java @@ -29,12 +29,15 @@ import me.lucko.luckperms.common.locale.TranslationManager; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.sender.SenderFactory; +import me.lucko.luckperms.fabric.mixin.ServerCommandSourceAccessor; import me.lucko.luckperms.fabric.model.MixinUser; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.luckperms.api.util.Tristate; +import net.minecraft.server.command.CommandOutput; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.rcon.RconCommandOutput; import net.minecraft.text.Text; import java.util.Locale; @@ -107,7 +110,10 @@ protected void performCommand(ServerCommandSource sender, String command) { @Override protected boolean isConsole(ServerCommandSource sender) { - return sender.getEntity() == null; + CommandOutput output = ((ServerCommandSourceAccessor) sender).getOutput(); + return output == sender.getServer() || // Console + output.getClass() == RconCommandOutput.class || // Rcon + (output == CommandOutput.DUMMY && sender.getName().equals("")); // Functions } public static Text toNativeText(Component component) { diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerCommandSourceAccessor.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerCommandSourceAccessor.java new file mode 100644 index 000000000..3418b859c --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerCommandSourceAccessor.java @@ -0,0 +1,42 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.fabric.mixin; + +import net.minecraft.server.command.CommandOutput; +import net.minecraft.server.command.ServerCommandSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * Accessor mixin to provide access to the underlying {@link CommandOutput} + */ +@Mixin(ServerCommandSource.class) +public interface ServerCommandSourceAccessor { + + @Accessor("output") + CommandOutput getOutput(); + +} diff --git a/fabric/src/main/resources/luckperms.mixins.json b/fabric/src/main/resources/luckperms.mixins.json index 24634c967..bc4c58c9b 100644 --- a/fabric/src/main/resources/luckperms.mixins.json +++ b/fabric/src/main/resources/luckperms.mixins.json @@ -4,6 +4,7 @@ "compatibilityLevel": "JAVA_8", "mixins": [ "CommandManagerMixin", + "ServerCommandSourceAccessor", "ServerLoginNetworkHandlerAccessor", "ServerPlayerEntityMixin" ], diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java index 911e17d45..83c6fdc19 100644 --- a/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java @@ -38,8 +38,10 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.luckperms.api.util.Tristate; +import net.minecraft.commands.CommandSource; import net.minecraft.commands.CommandSourceStack; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.rcon.RconConsoleSource; import net.minecraft.world.entity.player.Player; import java.util.Locale; @@ -107,7 +109,10 @@ protected void performCommand(CommandSourceStack sender, String command) { @Override protected boolean isConsole(CommandSourceStack sender) { - return !(sender.getEntity() instanceof Player); + CommandSource output = sender.source; + return output == sender.getServer() || // Console + output.getClass() == RconConsoleSource.class || // Rcon + (output == CommandSource.NULL && sender.getTextName().equals("")); // Functions } public static net.minecraft.network.chat.Component toNativeText(Component component) { From 5dd808752cf72a6d2babce9d5b6ac8d9ff9a3fd6 Mon Sep 17 00:00:00 2001 From: Luck Date: Wed, 14 Jun 2023 21:27:57 +0100 Subject: [PATCH 09/23] Update Forge/Fabric to 1.20.1 --- fabric/build.gradle | 6 +++--- forge/gradle.properties | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fabric/build.gradle b/fabric/build.gradle index f5b0c4e2c..461c88404 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -14,8 +14,8 @@ repositories { dependencies { // https://modmuss50.me/fabric.html - minecraft 'com.mojang:minecraft:1.20' - mappings 'net.fabricmc:yarn:1.20+build.1:v2' + minecraft 'com.mojang:minecraft:1.20.1' + mappings 'net.fabricmc:yarn:1.20.1+build.2:v2' modImplementation 'net.fabricmc:fabric-loader:0.14.21' Set apiModules = [ @@ -26,7 +26,7 @@ dependencies { ] apiModules.forEach { - modImplementation(fabricApi.module(it, '0.83.0+1.20')) + modImplementation(fabricApi.module(it, '0.83.1+1.20.1')) } include(modImplementation('me.lucko:fabric-permissions-api:0.2-SNAPSHOT')) diff --git a/forge/gradle.properties b/forge/gradle.properties index c7eb686d7..96afd9f7c 100644 --- a/forge/gradle.properties +++ b/forge/gradle.properties @@ -1,2 +1,2 @@ -minecraftVersion=1.20 -forgeVersion=46.0.1 \ No newline at end of file +minecraftVersion=1.20.1 +forgeVersion=47.0.1 \ No newline at end of file From 9f1e74fa7c5ce8226d92a9ab02aa383111c6c461 Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 18 Jun 2023 21:38:19 +0100 Subject: [PATCH 10/23] Fix user deletion not being correctly processed on SQL backends (#3664) --- .../common/model/nodemap/RecordedNodeMap.java | 8 +++ .../implementation/sql/SqlStorage.java | 7 +++ .../common/storage/SqlStorageTest.java | 61 +++++++++++++++++++ .../standalone/StorageIntegrationTest.java | 41 ++++++++++++- 4 files changed, 114 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/RecordedNodeMap.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/RecordedNodeMap.java index e655c2ccf..40a12aaf4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/RecordedNodeMap.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/RecordedNodeMap.java @@ -27,6 +27,8 @@ import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.model.manager.group.GroupManager; +import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.util.Difference; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; @@ -86,6 +88,12 @@ public Difference exportChanges(Predicate> onlyIf) { } } + public Difference addDefaultNodeToChangeSet() { + Difference diff = new Difference<>(); + diff.recordChange(Difference.ChangeType.ADD, Inheritance.builder(GroupManager.DEFAULT_GROUP_NAME).build()); + return record(diff); + } + private Difference record(Difference result) { this.lock.lock(); try { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java index 81409aa9c..ee68fb342 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SqlStorage.java @@ -399,6 +399,13 @@ public void saveUser(User user) throws SQLException { return true; }); + // if the user only has the default group, delete their data + boolean isDefaultUser = !this.plugin.getUserManager().isNonDefaultUser(user); + if (changes != null && isDefaultUser) { + user.normalData().addDefaultNodeToChangeSet(); + changes = null; + } + if (changes == null) { try (Connection c = this.connectionFactory.getConnection()) { deleteUser(c, user.getUniqueId()); diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/SqlStorageTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/SqlStorageTest.java index bb9836882..bcd45e79e 100644 --- a/common/src/test/java/me/lucko/luckperms/common/storage/SqlStorageTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/storage/SqlStorageTest.java @@ -28,20 +28,31 @@ import com.google.common.collect.ImmutableSet; import me.lucko.luckperms.common.actionlog.Log; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.LuckPermsConfiguration; import me.lucko.luckperms.common.event.EventDispatcher; import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.PrimaryGroupHolder; +import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.group.GroupManager; import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.model.manager.user.StandardUserManager; +import me.lucko.luckperms.common.model.manager.user.UserManager; +import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.node.types.Permission; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.file.NonClosableConnection; import net.luckperms.api.actionlog.Action; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.model.PlayerSaveResult.Outcome; +import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.node.types.PermissionNode; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -58,6 +69,7 @@ import java.util.function.Function; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; @@ -66,18 +78,25 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class SqlStorageTest { @Mock private LuckPermsPlugin plugin; @Mock private LuckPermsBootstrap bootstrap; + @Mock private LuckPermsConfiguration configuration; private SqlStorage storage; @BeforeEach public void setupMocksAndDatabase() throws Exception { lenient().when(this.plugin.getBootstrap()).thenReturn(this.bootstrap); + lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration); + lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + lenient().when(this.bootstrap.getScheduler()).thenReturn(mock(SchedulerAdapter.class)); + lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION)).thenReturn(PrimaryGroupHolder.AllParentsByWeight::new); + lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION_METHOD)).thenReturn("parents-by-weight"); lenient().when(this.bootstrap.getResourceStream(anyString())) .then(answer((String path) -> SqlStorageTest.class.getClassLoader().getResourceAsStream(path))); lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); @@ -197,6 +216,48 @@ public void testSaveAndLoadGroup() throws Exception { assertEquals(nodes, loaded.normalData().asSet()); } + @Test + public void testSaveAndDeleteUser() throws SQLException { + StandardUserManager userManager = new StandardUserManager(this.plugin); + + //noinspection unchecked,rawtypes + when(this.plugin.getUserManager()).thenReturn((UserManager) userManager); + + UUID exampleUniqueId = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"); + String exampleUsername = "Notch"; + PermissionNode examplePermission = Permission.builder() + .permission("test.1") + .withContext("server", "test") + .build(); + InheritanceNode defaultGroupNode = Inheritance.builder(GroupManager.DEFAULT_GROUP_NAME).build(); + + // create a default user, assert that is doesn't appear in unique users list + this.storage.savePlayerData(exampleUniqueId, exampleUsername); + assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId)); + + // give the user a node, assert that it does appear in unique users list + User user = this.storage.loadUser(exampleUniqueId, exampleUsername); + user.setNode(DataType.NORMAL, examplePermission, true); + this.storage.saveUser(user); + assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId)); + + // clear all nodes (reset to default) and assert that it does not appear in unique users list + user.clearNodes(DataType.NORMAL, null, true); + this.storage.saveUser(user); + assertFalse(this.storage.getUniqueUsers().contains(exampleUniqueId)); + assertEquals(ImmutableSet.of(defaultGroupNode), user.normalData().asSet()); + + // give it a node again, assert that it shows as a unique user + user.setNode(DataType.NORMAL, examplePermission, true); + this.storage.saveUser(user); + assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId)); + assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet()); + + // reload user data from the db and assert that it is unchanged + user = this.storage.loadUser(exampleUniqueId, exampleUsername); + assertEquals(ImmutableSet.of(defaultGroupNode, examplePermission), user.normalData().asSet()); + } + private static class TestH2ConnectionFactory implements ConnectionFactory { private final NonClosableConnection connection; diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java index da8bb281b..9a198c7ff 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java @@ -62,7 +62,11 @@ import java.util.Map; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; @Testcontainers public class StorageIntegrationTest { @@ -119,8 +123,10 @@ private static void testStorage(LuckPermsApplication app, TestPluginBootstrap bo // try to create / save a user UUID exampleUniqueId = UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"); - plugin.getStorage().savePlayerData(exampleUniqueId, "Luck").join(); - User user = plugin.getStorage().loadUser(exampleUniqueId, "Luck").join(); + String exampleUsername = "Luck"; + + plugin.getStorage().savePlayerData(exampleUniqueId, exampleUsername).join(); + User user = plugin.getStorage().loadUser(exampleUniqueId, exampleUsername).join(); user.setNode(DataType.NORMAL, TEST_PERMISSION_1, true); user.setNode(DataType.NORMAL, TEST_PERMISSION_2, true); user.setNode(DataType.NORMAL, TEST_GROUP, true); @@ -137,6 +143,35 @@ private static void testStorage(LuckPermsApplication app, TestPluginBootstrap bo User testUser = plugin.getStorage().loadUser(exampleUniqueId, null).join(); assertNotNull(testUser); assertEquals(ImmutableSet.of(Inheritance.builder("default").build(), TEST_PERMISSION_1, TEST_PERMISSION_2, TEST_GROUP, TEST_PREFIX, TEST_META), testUser.normalData().asSet()); + assertTrue(exampleUsername.equalsIgnoreCase(testUser.getUsername().orElse("unknown"))); + + + // create another user + UUID otherExampleUniqueId = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"); + String otherExampleUsername = "Notch"; + + plugin.getStorage().savePlayerData(otherExampleUniqueId, otherExampleUsername).join(); + assertFalse(plugin.getStorage().getUniqueUsers().join().contains(otherExampleUniqueId)); + + User otherUser = plugin.getStorage().loadUser(otherExampleUniqueId, otherExampleUsername).join(); + otherUser.setNode(DataType.NORMAL, TEST_PERMISSION_1, true); + plugin.getStorage().saveUser(otherUser).join(); + assertTrue(plugin.getStorage().getUniqueUsers().join().contains(otherExampleUniqueId)); + + otherUser.clearNodes(DataType.NORMAL, null, true); + plugin.getStorage().saveUser(otherUser).join(); + assertFalse(plugin.getStorage().getUniqueUsers().join().contains(otherExampleUniqueId)); + + + // test uuid/username lookup + assertEquals(otherExampleUniqueId, plugin.getStorage().getPlayerUniqueId(otherExampleUsername).join()); + assertTrue(otherExampleUsername.equalsIgnoreCase(plugin.getStorage().getPlayerName(otherExampleUniqueId).join())); + + plugin.getStorage().deletePlayerData(otherExampleUniqueId).join(); + assertNull(plugin.getStorage().getPlayerUniqueId(otherExampleUsername).join()); + assertNull(plugin.getStorage().getPlayerName(otherExampleUniqueId).join()); + assertNull(plugin.getStorage().getPlayerUniqueId("example").join()); + assertNull(plugin.getStorage().getPlayerName(UUID.randomUUID()).join()); } @Nested From 7a595364505570b7bdec0b9ed20eb2fc33f1b022 Mon Sep 17 00:00:00 2001 From: Dominik Date: Sat, 24 Jun 2023 00:30:22 +0200 Subject: [PATCH 11/23] Add support for redis cluster (#3670) --- bukkit/src/main/resources/config.yml | 1 + bungee/src/main/resources/config.yml | 1 + common/build.gradle | 2 +- .../luckperms/common/config/ConfigKeys.java | 6 ++ .../config/generic/key/ConfigKeyFactory.java | 6 ++ .../common/dependencies/Dependency.java | 4 +- .../common/messaging/MessagingFactory.java | 16 +++- .../messaging/redis/RedisMessenger.java | 92 +++++++++++++------ fabric/src/main/resources/luckperms.conf | 1 + forge/src/main/resources/luckperms.conf | 1 + nukkit/src/main/resources/config.yml | 1 + sponge/src/main/resources/luckperms.conf | 1 + standalone/build.gradle | 2 +- standalone/src/main/resources/config.yml | 1 + velocity/src/main/resources/config.yml | 1 + 15 files changed, 104 insertions(+), 32 deletions(-) diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index 155b310a7..cd6a0a5b9 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -262,6 +262,7 @@ broadcast-received-log-entries: true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index 3e0633fff..4d20818a8 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -260,6 +260,7 @@ broadcast-received-log-entries: false # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost diff --git a/common/build.gradle b/common/build.gradle index 5958da14a..d27b3139c 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -74,7 +74,7 @@ dependencies { transitive = false } compileOnly 'com.zaxxer:HikariCP:4.0.3' - compileOnly 'redis.clients:jedis:3.5.2' + compileOnly 'redis.clients:jedis:4.4.3' compileOnly 'io.nats:jnats:2.16.4' compileOnly 'com.rabbitmq:amqp-client:5.12.0' compileOnly 'org.mongodb:mongodb-driver-legacy:4.5.0' diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java index 221e515dd..dcdea307c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java @@ -73,6 +73,7 @@ import static me.lucko.luckperms.common.config.generic.key.ConfigKeyFactory.mapKey; import static me.lucko.luckperms.common.config.generic.key.ConfigKeyFactory.notReloadable; import static me.lucko.luckperms.common.config.generic.key.ConfigKeyFactory.stringKey; +import static me.lucko.luckperms.common.config.generic.key.ConfigKeyFactory.stringListKey; /** * All of the {@link ConfigKey}s used by LuckPerms. @@ -639,6 +640,11 @@ private ConfigKeys() {} */ public static final ConfigKey REDIS_ADDRESS = notReloadable(stringKey("redis.address", null)); + /** + * The addresses of the redis servers (only for redis clusters) + */ + public static final ConfigKey> REDIS_ADDRESSES = notReloadable(stringListKey("redis.addresses", ImmutableList.of())); + /** * The username to connect with, or an empty string if it should use default */ diff --git a/common/src/main/java/me/lucko/luckperms/common/config/generic/key/ConfigKeyFactory.java b/common/src/main/java/me/lucko/luckperms/common/config/generic/key/ConfigKeyFactory.java index cd7da5a0c..add003435 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/generic/key/ConfigKeyFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/generic/key/ConfigKeyFactory.java @@ -28,6 +28,7 @@ import com.google.common.collect.ImmutableMap; import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -36,6 +37,7 @@ public interface ConfigKeyFactory { ConfigKeyFactory BOOLEAN = ConfigurationAdapter::getBoolean; ConfigKeyFactory STRING = ConfigurationAdapter::getString; + ConfigKeyFactory> STRING_LIST = ConfigurationAdapter::getStringList; ConfigKeyFactory LOWERCASE_STRING = (adapter, path, def) -> adapter.getString(path, def).toLowerCase(Locale.ROOT); ConfigKeyFactory> STRING_MAP = (config, path, def) -> ImmutableMap.copyOf(config.getStringMap(path, ImmutableMap.of())); @@ -56,6 +58,10 @@ static SimpleConfigKey stringKey(String path, String def) { return key(new Bound<>(STRING, path, def)); } + static SimpleConfigKey> stringListKey(String path, List def) { + return key(new Bound<>(STRING_LIST, path, def)); + } + static SimpleConfigKey lowercaseStringKey(String path, String def) { return key(new Bound<>(LOWERCASE_STRING, path, def)); } diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java index 3ec7fa6b4..bc4a66c23 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java @@ -239,8 +239,8 @@ public enum Dependency { JEDIS( "redis.clients", "jedis", - "3.5.2", - "jX3340YaYjHFQN2sA+GCo33LB4FuIYKgQUPUv2MK/Xo=", + "4.4.3", + "wwwoCDPCywcfoNwpvwP95kXYusXSTtXhuVrB31sxE0k=", Relocation.of("jedis", "redis{}clients{}jedis"), Relocation.of("commonspool2", "org{}apache{}commons{}pool2") ), diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/MessagingFactory.java b/common/src/main/java/me/lucko/luckperms/common/messaging/MessagingFactory.java index c7a6271e5..7a2800779 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/MessagingFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/MessagingFactory.java @@ -43,6 +43,8 @@ import net.luckperms.api.messenger.MessengerProvider; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; public class MessagingFactory

{ @@ -194,6 +196,7 @@ private class RedisMessengerProvider implements MessengerProvider { LuckPermsConfiguration config = getPlugin().getConfiguration(); String address = config.get(ConfigKeys.REDIS_ADDRESS); + List addresses = config.get(ConfigKeys.REDIS_ADDRESSES); String username = config.get(ConfigKeys.REDIS_USERNAME); String password = config.get(ConfigKeys.REDIS_PASSWORD); if (password.isEmpty()) { @@ -204,7 +207,18 @@ private class RedisMessengerProvider implements MessengerProvider { } boolean ssl = config.get(ConfigKeys.REDIS_SSL); - redis.init(address, username, password, ssl); + if (!addresses.isEmpty()) { + // redis cluster + addresses = new ArrayList<>(addresses); + if (address != null) { + addresses.add(address); + } + redis.init(addresses, username, password, ssl); + } else { + // redis pool + redis.init(address, username, password, ssl); + } + return redis; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java index 8fec0f7d7..e9e5ff69d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java @@ -30,11 +30,21 @@ import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.message.OutgoingMessage; import org.checkerframework.checker.nullness.qual.NonNull; -import redis.clients.jedis.Jedis; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisPooled; import redis.clients.jedis.JedisPubSub; import redis.clients.jedis.Protocol; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.exceptions.JedisClusterOperationException; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; /** * An implementation of {@link Messenger} using Redis. @@ -45,7 +55,7 @@ public class RedisMessenger implements Messenger { private final LuckPermsPlugin plugin; private final IncomingMessageConsumer consumer; - private /* final */ JedisPool jedisPool; + private /* final */ UnifiedJedis jedis; private /* final */ Subscription sub; private boolean closing = false; @@ -54,57 +64,74 @@ public RedisMessenger(LuckPermsPlugin plugin, IncomingMessageConsumer consumer) this.consumer = consumer; } + public void init(List addresses, String username, String password, boolean ssl) { + Set hosts = addresses.stream().map(RedisMessenger::parseAddress).collect(Collectors.toSet()); + this.init(new JedisCluster(hosts, jedisConfig(username, password, ssl))); + } + public void init(String address, String username, String password, boolean ssl) { + this.init(new JedisPooled(parseAddress(address), jedisConfig(username, password, ssl))); + } + + private void init(UnifiedJedis jedis) { + this.jedis = jedis; + this.sub = new Subscription(this); + this.plugin.getBootstrap().getScheduler().executeAsync(this.sub); + } + + private static JedisClientConfig jedisConfig(String username, String password, boolean ssl) { + return DefaultJedisClientConfig.builder() + .user(username) + .password(password) + .ssl(ssl) + .timeoutMillis(Protocol.DEFAULT_TIMEOUT) + .build(); + } + + private static HostAndPort parseAddress(String address) { String[] addressSplit = address.split(":"); String host = addressSplit[0]; int port = addressSplit.length > 1 ? Integer.parseInt(addressSplit[1]) : Protocol.DEFAULT_PORT; - - if (username == null) { - this.jedisPool = new JedisPool(new JedisPoolConfig(), host, port, Protocol.DEFAULT_TIMEOUT, password, ssl); - } else { - this.jedisPool = new JedisPool(new JedisPoolConfig(), host, port, Protocol.DEFAULT_TIMEOUT, username, password, ssl); - } - - this.sub = new Subscription(); - this.plugin.getBootstrap().getScheduler().executeAsync(this.sub); + return new HostAndPort(host, port); } @Override public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { - try (Jedis jedis = this.jedisPool.getResource()) { - jedis.publish(CHANNEL, outgoingMessage.asEncodedString()); - } catch (Exception e) { - e.printStackTrace(); - } + this.jedis.publish(CHANNEL, outgoingMessage.asEncodedString()); } @Override public void close() { this.closing = true; this.sub.unsubscribe(); - this.jedisPool.destroy(); + this.jedis.close(); } - private class Subscription extends JedisPubSub implements Runnable { + private static class Subscription extends JedisPubSub implements Runnable { + private final RedisMessenger messenger; + + private Subscription(RedisMessenger messenger) { + this.messenger = messenger; + } @Override public void run() { boolean first = true; - while (!RedisMessenger.this.closing && !Thread.interrupted() && !RedisMessenger.this.jedisPool.isClosed()) { - try (Jedis jedis = RedisMessenger.this.jedisPool.getResource()) { + while (!this.messenger.closing && !Thread.interrupted() && this.isRedisAlive()) { + try { if (first) { first = false; } else { - RedisMessenger.this.plugin.getLogger().info("Redis pubsub connection re-established"); + this.messenger.plugin.getLogger().info("Redis pubsub connection re-established"); } - jedis.subscribe(this, CHANNEL); // blocking call + this.messenger.jedis.subscribe(this, CHANNEL); // blocking call } catch (Exception e) { - if (RedisMessenger.this.closing) { + if (this.messenger.closing) { return; } - RedisMessenger.this.plugin.getLogger().warn("Redis pubsub connection dropped, trying to re-open the connection", e); + this.messenger.plugin.getLogger().warn("Redis pubsub connection dropped, trying to re-open the connection", e); try { unsubscribe(); } catch (Exception ignored) { @@ -126,8 +153,19 @@ public void onMessage(String channel, String msg) { if (!channel.equals(CHANNEL)) { return; } - RedisMessenger.this.consumer.consumeIncomingMessageAsString(msg); + this.messenger.consumer.consumeIncomingMessageAsString(msg); } - } + private boolean isRedisAlive() { + UnifiedJedis jedis = this.messenger.jedis; + + if (jedis instanceof JedisPooled) { + return !((JedisPooled) jedis).getPool().isClosed(); + } else if (jedis instanceof JedisCluster) { + return !((JedisCluster) jedis).getClusterNodes().isEmpty(); + } else { + throw new RuntimeException("Unknown jedis type: " + jedis.getClass().getName()); + } + } + } } diff --git a/fabric/src/main/resources/luckperms.conf b/fabric/src/main/resources/luckperms.conf index 6045f1ebc..1b74e9d07 100644 --- a/fabric/src/main/resources/luckperms.conf +++ b/fabric/src/main/resources/luckperms.conf @@ -265,6 +265,7 @@ broadcast-received-log-entries = true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis { enabled = false address = "localhost" diff --git a/forge/src/main/resources/luckperms.conf b/forge/src/main/resources/luckperms.conf index 0093326c8..63e9e1876 100644 --- a/forge/src/main/resources/luckperms.conf +++ b/forge/src/main/resources/luckperms.conf @@ -263,6 +263,7 @@ broadcast-received-log-entries = true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis { enabled = false address = "localhost" diff --git a/nukkit/src/main/resources/config.yml b/nukkit/src/main/resources/config.yml index d77169a93..4eff4cb3f 100644 --- a/nukkit/src/main/resources/config.yml +++ b/nukkit/src/main/resources/config.yml @@ -257,6 +257,7 @@ broadcast-received-log-entries: true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost diff --git a/sponge/src/main/resources/luckperms.conf b/sponge/src/main/resources/luckperms.conf index 029c69892..1493b117f 100644 --- a/sponge/src/main/resources/luckperms.conf +++ b/sponge/src/main/resources/luckperms.conf @@ -265,6 +265,7 @@ broadcast-received-log-entries = true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis { enabled = false address = "localhost" diff --git a/standalone/build.gradle b/standalone/build.gradle index c44f0c862..4db3fbfa6 100644 --- a/standalone/build.gradle +++ b/standalone/build.gradle @@ -29,7 +29,7 @@ dependencies { testImplementation 'org.awaitility:awaitility:4.2.0' testImplementation 'com.zaxxer:HikariCP:4.0.3' - testImplementation 'redis.clients:jedis:3.5.2' + testImplementation 'redis.clients:jedis:4.4.3' testImplementation 'io.nats:jnats:2.16.4' testImplementation 'com.rabbitmq:amqp-client:5.12.0' testImplementation 'org.postgresql:postgresql:42.6.0' diff --git a/standalone/src/main/resources/config.yml b/standalone/src/main/resources/config.yml index d9fe05ecb..fe72e1d72 100644 --- a/standalone/src/main/resources/config.yml +++ b/standalone/src/main/resources/config.yml @@ -247,6 +247,7 @@ broadcast-received-log-entries: true # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost diff --git a/velocity/src/main/resources/config.yml b/velocity/src/main/resources/config.yml index dcd8ac6fd..6c8b0608d 100644 --- a/velocity/src/main/resources/config.yml +++ b/velocity/src/main/resources/config.yml @@ -251,6 +251,7 @@ broadcast-received-log-entries: false # Settings for Redis. # Port 6379 is used by default; set address to "host:port" if differs +# Multiple Redis nodes can be specified in the same format as a string list under the name "addresses". redis: enabled: false address: localhost From feef08a2564c63a1da6c97a5127533fd4f594c47 Mon Sep 17 00:00:00 2001 From: Mart Date: Fri, 23 Jun 2023 18:34:11 -0400 Subject: [PATCH 12/23] Update GitHub URLs to new location (#3686) --- CONTRIBUTING.md | 2 +- README.md | 4 ++-- .../main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java | 2 +- bukkit/src/main/resources/config.yml | 2 +- bungee/src/main/resources/config.yml | 2 +- .../luckperms/common/plugin/AbstractLuckPermsPlugin.java | 2 +- .../java/me/lucko/luckperms/common/util/UniqueIdType.java | 4 ++-- fabric/src/main/resources/fabric.mod.json | 4 ++-- fabric/src/main/resources/luckperms.conf | 2 +- forge/src/main/resources/luckperms.conf | 2 +- .../nukkit/inject/permissible/PermissibleInjector.java | 2 +- .../luckperms/nukkit/listeners/NukkitConnectionListener.java | 2 +- nukkit/src/main/resources/config.yml | 2 +- sponge/src/main/resources/luckperms.conf | 2 +- standalone/src/main/resources/config.yml | 2 +- velocity/src/main/resources/config.yml | 2 +- 16 files changed, 19 insertions(+), 19 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f55363882..278998629 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Before reporting a bug or issue, please make sure that the issue is actually bei If you're unsure, feel free to ask using the above resources BEFORE making a report. -Bugs or issues should be reported using the [GitHub Issues tab](https://github.com/lucko/LuckPerms/issues). +Bugs or issues should be reported using the [GitHub Issues tab](https://github.com/LuckPerms/LuckPerms/issues). ### :pencil: Want to contribute code? #### Pull Requests diff --git a/README.md b/README.md index 2dae793b8..8782a9c97 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ LuckPerms uses Gradle to handle dependencies & building. #### Compiling from source ```sh -git clone https://github.com/lucko/LuckPerms.git +git clone https://github.com/LuckPerms/LuckPerms.git cd LuckPerms/ ./gradlew build ``` @@ -56,4 +56,4 @@ The project is split up into a few separate modules. * **Bukkit, BungeeCord, Fabric, Forge, Nukkit, Sponge & Velocity** - Each use the common module to implement plugins on the respective server platforms. ## License -LuckPerms is licensed under the permissive MIT license. Please see [`LICENSE.txt`](https://github.com/lucko/LuckPerms/blob/master/LICENSE.txt) for more info. +LuckPerms is licensed under the permissive MIT license. Please see [`LICENSE.txt`](https://github.com/LuckPerms/LuckPerms/blob/master/LICENSE.txt) for more info. diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java index 4d46e15e1..bcba22774 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java @@ -217,7 +217,7 @@ protected void setupPlatformHooks() { * Vault in their onEnable without depending on us. * * Noteworthy discussion here: - * - https://github.com/lucko/LuckPerms/issues/1959 + * - https://github.com/LuckPerms/LuckPerms/issues/1959 * - https://hub.spigotmc.org/jira/browse/SPIGOT-5546 * - https://github.com/PaperMC/Paper/pull/3509 */ diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index cd6a0a5b9..d65b1a5b9 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -8,7 +8,7 @@ # | | # # | WIKI: https://luckperms.net/wiki | # # | DISCORD: https://discord.gg/luckperms | # -# | BUG REPORTS: https://github.com/lucko/LuckPerms/issues | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # # | | # # | Each option in this file is documented and explained here: | # # | ==> https://luckperms.net/wiki/Configuration | # diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index 4d20818a8..be3313f0c 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -8,7 +8,7 @@ # | | # # | WIKI: https://luckperms.net/wiki | # # | DISCORD: https://discord.gg/luckperms | # -# | BUG REPORTS: https://github.com/lucko/LuckPerms/issues | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # # | | # # | Each option in this file is documented and explained here: | # # | ==> https://luckperms.net/wiki/Configuration | # diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java index 2f4b14cf7..fed88bb39 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java @@ -175,7 +175,7 @@ public final void enable() { this.fileWatcher = new FileWatcher(this, getBootstrap().getDataDirectory()); } catch (Throwable e) { // catch throwable here, seems some JVMs throw UnsatisfiedLinkError when trying - // to create a watch service. see: https://github.com/lucko/LuckPerms/issues/2066 + // to create a watch service. see: https://github.com/LuckPerms/LuckPerms/issues/2066 getLogger().warn("Error occurred whilst trying to create a file watcher:", e); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/util/UniqueIdType.java b/common/src/main/java/me/lucko/luckperms/common/util/UniqueIdType.java index e31ccd814..a01ae5a29 100644 --- a/common/src/main/java/me/lucko/luckperms/common/util/UniqueIdType.java +++ b/common/src/main/java/me/lucko/luckperms/common/util/UniqueIdType.java @@ -79,8 +79,8 @@ public static UniqueIdType determineType(UUID uniqueId, LuckPermsPlugin plugin) break; case 2: // if the uuid is version 2, assume it is an NPC - // see: https://github.com/lucko/LuckPerms/issues/1470 - // and https://github.com/lucko/LuckPerms/issues/1470#issuecomment-475403162 + // see: https://github.com/LuckPerms/LuckPerms/issues/1470 + // and https://github.com/LuckPerms/LuckPerms/issues/1470#issuecomment-475403162 type = UniqueIdDetermineTypeEvent.TYPE_NPC; break; default: diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 0121aa4e1..e820b2a6b 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -17,8 +17,8 @@ "license": "MIT", "contact": { "homepage": "https://luckperms.net", - "source": "https://github.com/lucko/LuckPerms", - "issues": "https://github.com/lucko/LuckPerms/issues" + "source": "https://github.com/LuckPerms/LuckPerms", + "issues": "https://github.com/LuckPerms/LuckPerms/issues" }, "environment": "server", "entrypoints": { diff --git a/fabric/src/main/resources/luckperms.conf b/fabric/src/main/resources/luckperms.conf index 1b74e9d07..29517a215 100644 --- a/fabric/src/main/resources/luckperms.conf +++ b/fabric/src/main/resources/luckperms.conf @@ -8,7 +8,7 @@ # | | # # | WIKI: https://luckperms.net/wiki | # # | DISCORD: https://discord.gg/luckperms | # -# | BUG REPORTS: https://github.com/lucko/LuckPerms/issues | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # # | | # # | Each option in this file is documented and explained here: | # # | ==> https://luckperms.net/wiki/Configuration | # diff --git a/forge/src/main/resources/luckperms.conf b/forge/src/main/resources/luckperms.conf index 63e9e1876..06e3c0d97 100644 --- a/forge/src/main/resources/luckperms.conf +++ b/forge/src/main/resources/luckperms.conf @@ -8,7 +8,7 @@ # | | # # | WIKI: https://luckperms.net/wiki | # # | DISCORD: https://discord.gg/luckperms | # -# | BUG REPORTS: https://github.com/lucko/LuckPerms/issues | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # # | | # # | Each option in this file is documented and explained here: | # # | ==> https://luckperms.net/wiki/Configuration | # diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/PermissibleInjector.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/PermissibleInjector.java index 895ae6032..346ae83c2 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/PermissibleInjector.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/PermissibleInjector.java @@ -82,7 +82,7 @@ public static void inject(Player player, LuckPermsPermissible newPermissible) th if (oldPermissible instanceof LuckPermsPermissible) { // Nukkit seems to re-use player instances (or perhaps calls the login event twice?) // so, just uninject here instead of throwing an exception like we do on Bukkit - // See: https://github.com/lucko/LuckPerms/issues/2791 + // See: https://github.com/LuckPerms/LuckPerms/issues/2791 uninject(player, false); } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/listeners/NukkitConnectionListener.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/listeners/NukkitConnectionListener.java index b12432af2..110c46a41 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/listeners/NukkitConnectionListener.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/listeners/NukkitConnectionListener.java @@ -193,7 +193,7 @@ public void onPlayerLoginMonitor(PlayerLoginEvent e) { public void onPlayerQuit(PlayerQuitEvent e) { final Player player = e.getPlayer(); - // https://github.com/lucko/LuckPerms/issues/2269 + // https://github.com/LuckPerms/LuckPerms/issues/2269 if (player.getUniqueId() == null) { return; } diff --git a/nukkit/src/main/resources/config.yml b/nukkit/src/main/resources/config.yml index 4eff4cb3f..a176bf3f6 100644 --- a/nukkit/src/main/resources/config.yml +++ b/nukkit/src/main/resources/config.yml @@ -8,7 +8,7 @@ # | | # # | WIKI: https://luckperms.net/wiki | # # | DISCORD: https://discord.gg/luckperms | # -# | BUG REPORTS: https://github.com/lucko/LuckPerms/issues | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # # | | # # | Each option in this file is documented and explained here: | # # | ==> https://luckperms.net/wiki/Configuration | # diff --git a/sponge/src/main/resources/luckperms.conf b/sponge/src/main/resources/luckperms.conf index 1493b117f..1d64efd49 100644 --- a/sponge/src/main/resources/luckperms.conf +++ b/sponge/src/main/resources/luckperms.conf @@ -8,7 +8,7 @@ # | | # # | WIKI: https://luckperms.net/wiki | # # | DISCORD: https://discord.gg/luckperms | # -# | BUG REPORTS: https://github.com/lucko/LuckPerms/issues | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # # | | # # | Each option in this file is documented and explained here: | # # | ==> https://luckperms.net/wiki/Configuration | # diff --git a/standalone/src/main/resources/config.yml b/standalone/src/main/resources/config.yml index fe72e1d72..e685caabb 100644 --- a/standalone/src/main/resources/config.yml +++ b/standalone/src/main/resources/config.yml @@ -8,7 +8,7 @@ # | | # # | WIKI: https://luckperms.net/wiki | # # | DISCORD: https://discord.gg/luckperms | # -# | BUG REPORTS: https://github.com/lucko/LuckPerms/issues | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # # | | # # | Each option in this file is documented and explained here: | # # | ==> https://luckperms.net/wiki/Configuration | # diff --git a/velocity/src/main/resources/config.yml b/velocity/src/main/resources/config.yml index 6c8b0608d..b7d79df27 100644 --- a/velocity/src/main/resources/config.yml +++ b/velocity/src/main/resources/config.yml @@ -8,7 +8,7 @@ # | | # # | WIKI: https://luckperms.net/wiki | # # | DISCORD: https://discord.gg/luckperms | # -# | BUG REPORTS: https://github.com/lucko/LuckPerms/issues | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # # | | # # | Each option in this file is documented and explained here: | # # | ==> https://luckperms.net/wiki/Configuration | # From 86cc90f48a1e7f87eed36233dc548306d19a08cb Mon Sep 17 00:00:00 2001 From: powercas_gamer Date: Sat, 24 Jun 2023 01:15:03 +0200 Subject: [PATCH 13/23] Refactor gradle plugin version handling (#3665) --- .github/workflows/ci.yml | 3 +++ build.gradle | 2 +- bukkit-legacy/build.gradle | 2 +- bukkit-legacy/loader/build.gradle | 2 +- bukkit/build.gradle | 2 +- bukkit/loader/build.gradle | 2 +- bungee/build.gradle | 2 +- bungee/loader/build.gradle | 2 +- common/build.gradle | 2 +- fabric/build.gradle | 5 ++--- forge/build.gradle | 6 +++--- forge/forge-api/build.gradle | 2 +- forge/loader/build.gradle | 6 +++--- gradle/libs.versions.toml | 13 +++++++++++++ nukkit/build.gradle | 2 +- nukkit/loader/build.gradle | 2 +- settings.gradle | 6 +++++- sponge/build.gradle | 2 +- sponge/loader/build.gradle | 2 +- standalone/app/build.gradle | 6 +++--- standalone/build.gradle | 2 +- standalone/loader/build.gradle | 4 ++-- velocity/build.gradle | 4 ++-- 23 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2391ba1f6..16bf9b016 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: with: fetch-depth: 0 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v1 + - uses: actions/setup-java@v3 with: distribution: 'temurin' diff --git a/build.gradle b/build.gradle index d8ac4cb96..cf81bd37e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.cadixdev.licenser' version '0.6.1' apply false + alias(libs.plugins.licenser) apply false } defaultTasks 'licenseFormat', 'build' diff --git a/bukkit-legacy/build.gradle b/bukkit-legacy/build.gradle index 3e21ee1a6..2cbdccf4a 100644 --- a/bukkit-legacy/build.gradle +++ b/bukkit-legacy/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' + alias(libs.plugins.shadow) } dependencies { diff --git a/bukkit-legacy/loader/build.gradle b/bukkit-legacy/loader/build.gradle index 65bab3fdd..92b9a4e1d 100644 --- a/bukkit-legacy/loader/build.gradle +++ b/bukkit-legacy/loader/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' + alias(libs.plugins.shadow) } repositories { diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 9569036d8..db57631ab 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' + alias(libs.plugins.shadow) } repositories { diff --git a/bukkit/loader/build.gradle b/bukkit/loader/build.gradle index 31b83217c..c41f2a81c 100644 --- a/bukkit/loader/build.gradle +++ b/bukkit/loader/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' + alias(libs.plugins.shadow) } repositories { diff --git a/bungee/build.gradle b/bungee/build.gradle index 6ea4a7cb5..f46056304 100644 --- a/bungee/build.gradle +++ b/bungee/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' + alias(libs.plugins.shadow) } dependencies { diff --git a/bungee/loader/build.gradle b/bungee/loader/build.gradle index 10de5e9e5..de0e40b1f 100644 --- a/bungee/loader/build.gradle +++ b/bungee/loader/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' + alias(libs.plugins.shadow) } dependencies { diff --git a/common/build.gradle b/common/build.gradle index d27b3139c..c6f318ff5 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'java-library' + id("java-library") } test { diff --git a/fabric/build.gradle b/fabric/build.gradle index 461c88404..36e7c5f60 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -1,15 +1,14 @@ import net.fabricmc.loom.task.RemapJarTask plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'fabric-loom' version '1.2-SNAPSHOT' + alias(libs.plugins.shadow) + alias(libs.plugins.loom) } archivesBaseName = 'luckperms' repositories { maven { url 'https://maven.fabricmc.net/' } - mavenLocal() } dependencies { diff --git a/forge/build.gradle b/forge/build.gradle index 1d79b1370..b5d9f80e0 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'net.kyori.blossom' version '1.3.0' - id 'net.minecraftforge.gradle' version '[6.0,6.2)' + alias(libs.plugins.blossom) + alias(libs.plugins.shadow) + alias(libs.plugins.forgegradle) } sourceCompatibility = 1.8 diff --git a/forge/forge-api/build.gradle b/forge/forge-api/build.gradle index 71df388c5..cf210c2ea 100644 --- a/forge/forge-api/build.gradle +++ b/forge/forge-api/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'net.minecraftforge.gradle' version '[6.0,6.2)' + alias(libs.plugins.forgegradle) } sourceCompatibility = 1.8 diff --git a/forge/loader/build.gradle b/forge/loader/build.gradle index 85242c1bb..1b6f0040d 100644 --- a/forge/loader/build.gradle +++ b/forge/loader/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'java-library' - id 'net.minecraftforge.gradle' version '[6.0,6.2)' + alias(libs.plugins.shadow) + alias(libs.plugins.forgegradle) + id("java-library") } sourceCompatibility = 1.8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..097413772 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,13 @@ +[versions] +shadow = "8.1.1" +blossom = "1.3.1" +forgegradle = "[6.0,6.2)" +loom = "1.2-SNAPSHOT" +licenser = "0.6.1" + +[plugins] +blossom = { id = "net.kyori.blossom", version.ref = "blossom" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } +forgegradle = { id = "net.minecraftforge.gradle", version.ref = "forgegradle" } +loom = { id = "fabric-loom", version.ref = "loom" } +licenser = { id = "org.cadixdev.licenser", version.ref = "licenser" } \ No newline at end of file diff --git a/nukkit/build.gradle b/nukkit/build.gradle index 92c0233aa..a30745eb1 100644 --- a/nukkit/build.gradle +++ b/nukkit/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' + alias(libs.plugins.shadow) } repositories { diff --git a/nukkit/loader/build.gradle b/nukkit/loader/build.gradle index 15d5c873b..8d0211ebd 100644 --- a/nukkit/loader/build.gradle +++ b/nukkit/loader/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' + alias(libs.plugins.shadow) } repositories { diff --git a/settings.gradle b/settings.gradle index 2f64daeb3..91cdf3aa2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,7 +2,7 @@ pluginManagement { repositories { maven { name = 'Fabric' - url 'https://maven.fabricmc.net/' + url = 'https://maven.fabricmc.net/' } maven { name = 'Forge' @@ -12,6 +12,10 @@ pluginManagement { } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version("0.5.0") +} + rootProject.name = 'luckperms' include ( 'api', diff --git a/sponge/build.gradle b/sponge/build.gradle index f618eaff3..37490efbc 100644 --- a/sponge/build.gradle +++ b/sponge/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' + alias(libs.plugins.shadow) } repositories { diff --git a/sponge/loader/build.gradle b/sponge/loader/build.gradle index b7a89d919..14499ff80 100644 --- a/sponge/loader/build.gradle +++ b/sponge/loader/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' + alias(libs.plugins.shadow) } repositories { diff --git a/standalone/app/build.gradle b/standalone/app/build.gradle index cd9dd95f9..aa2ab7bb8 100644 --- a/standalone/app/build.gradle +++ b/standalone/app/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'net.kyori.blossom' version '1.3.0' - id 'java-library' + alias(libs.plugins.blossom) + id("java-library") } sourceCompatibility = 17 @@ -36,7 +36,7 @@ dependencies { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') } - api('net.kyori:ansi:1.0.0-SNAPSHOT') + api('net.kyori:ansi:1.0.1') } blossom { diff --git a/standalone/build.gradle b/standalone/build.gradle index 4db3fbfa6..6713ddb9f 100644 --- a/standalone/build.gradle +++ b/standalone/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.github.johnrengelman.shadow' version '8.1.1' + alias(libs.plugins.shadow) } sourceCompatibility = 17 diff --git a/standalone/loader/build.gradle b/standalone/loader/build.gradle index c973a84ff..729ef4c31 100644 --- a/standalone/loader/build.gradle +++ b/standalone/loader/build.gradle @@ -1,8 +1,8 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer plugins { - id 'com.github.johnrengelman.shadow' - id 'application' + alias(libs.plugins.shadow) + id("application") } dependencies { diff --git a/velocity/build.gradle b/velocity/build.gradle index e255e729f..fb35079cd 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'net.kyori.blossom' version '1.3.0' - id 'com.github.johnrengelman.shadow' version '8.1.1' + alias(libs.plugins.blossom) + alias(libs.plugins.shadow) } repositories { From 35dfc2c8f5e7abfac7ccb8a25c059f6530b000a8 Mon Sep 17 00:00:00 2001 From: powercas_gamer Date: Sat, 24 Jun 2023 01:24:16 +0200 Subject: [PATCH 14/23] Render minimessage in prefix/suffix/meta values (#3672) --- common/build.gradle | 5 + .../common/dependencies/Dependency.java | 17 ++- .../luckperms/common/locale/Message.java | 104 ++++++++++-------- standalone/app/build.gradle | 4 + 4 files changed, 77 insertions(+), 53 deletions(-) diff --git a/common/build.gradle b/common/build.gradle index c6f318ff5..a8b533e02 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -45,6 +45,11 @@ dependencies { exclude(module: 'adventure-api') } + api("net.kyori:adventure-text-minimessage:4.11.0") { + exclude(module: 'adventure-bom') + exclude(module: 'adventure-api') + } + api('net.kyori:event-api:3.0.0') { exclude(module: 'checker-qual') exclude(module: 'guava') diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java index bc4a66c23..99407c306 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/Dependency.java @@ -59,33 +59,32 @@ public enum Dependency { "1.7", "b30RhOF6kHiHl+O5suNLh/+eAr1iOFEFLXhwkHHDu4I=" ), - ADVENTURE( "me{}lucko", "adventure-api", - "4.11.0", - "7xrFaBbsTiQPZKBDzvDnTd8XIgOsHTy9qQICm3342GU=", + "4.13.0", + "gcmYlY1KDrzbnE9nB8rA44oBZKPSMvv9kNRefoEuud0=", Relocation.of("adventure", "net{}kyori{}adventure") ), ADVENTURE_PLATFORM( "me{}lucko", "adventure-platform-api", - "4.11.2", - "zCnxNgosme++TsheFaL+YHdtRIrp+oJhUiI8awsKgfQ=", + "4.13.0", + "Kk8IkEMVa9ITBfC3yocpcXQiZ9CwN9VxeWjKUD8I0n0=", Relocation.of("adventure", "net{}kyori{}adventure") ), ADVENTURE_PLATFORM_BUKKIT( "me{}lucko", "adventure-platform-bukkit", - "4.11.2", - "mrtYZvt00GH4oejuBJ2QEbFkmz1F+PGmthPwroSxCgo=", + "4.13.0", + "Rif/+xdGfRrlhxXYb2+jMFBnwnLQ2pnjAJ/BxWApins=", Relocation.of("adventure", "net{}kyori{}adventure") ), ADVENTURE_PLATFORM_BUNGEECORD( "me{}lucko", "adventure-platform-bungeecord", - "4.11.2", - "+WUdRdZ6qkacw3ha/R3ayLx46soMywGe70Zmnw4yha8=", + "4.13.0", + "MLk/qAgWC9YT2ImLq/sVo114V5Rk1jQr4jc57WAoO74=", Relocation.of("adventure", "net{}kyori{}adventure") ), EVENT( diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java index d192d9cbf..3a6ea5f56 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java @@ -39,9 +39,11 @@ import me.lucko.luckperms.common.util.DurationFormatter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextSet; @@ -96,7 +98,7 @@ public interface Message { TextComponent OPEN_BRACKET = Component.text('('); TextComponent CLOSE_BRACKET = Component.text(')'); TextComponent FULL_STOP = Component.text('.'); - + Component PREFIX_COMPONENT = text() .color(GRAY) .append(text('[')) @@ -135,7 +137,7 @@ static TextComponent prefixed(ComponentLike component) { // " | |__) " // " |___ | " - return join(newline(), + return joinNewline( text() .append(text(" ", AQUA)) .append(text(" __ ", DARK_AQUA)) @@ -175,7 +177,7 @@ static TextComponent prefixed(ComponentLike component) { .color(GRAY) ); - Args2 FIRST_TIME_SETUP = (label, username) -> join(newline(), + Args2 FIRST_TIME_SETUP = (label, username) -> joinNewline( // "&3It seems that no permissions have been setup yet!" // "&3Before you can use any of the LuckPerms commands in-game, you need to use the console to give yourself access." // "&3Open your console and run:" @@ -278,7 +280,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args1 LOG = action -> join(newline(), + Args1 LOG = action -> joinNewline( // "&3LOG &3&l> &8(&e{}&8) [&a{}&8] (&b{}&8)" // "&3LOG &3&l> &f{}" prefixed(text() @@ -489,7 +491,7 @@ static TextComponent prefixed(ComponentLike component) { .append(CLOSE_BRACKET) )); - Args2 COMMAND_USAGE_DETAILED_HEADER = (name, usage) -> join(newline(), + Args2 COMMAND_USAGE_DETAILED_HEADER = (name, usage) -> joinNewline( // "&3&lCommand Usage &3- &b{}" // "&b> &7{}" prefixed(text() @@ -666,7 +668,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args0 VERBOSE_OFF_COMMAND_NO_CHECKS = () -> join(newline(), + Args0 VERBOSE_OFF_COMMAND_NO_CHECKS = () -> joinNewline( // &bThe command execution completed, but no permission checks were made. // &7This might be because the plugin runs commands in the background (async). You can still use verbose manually to detect checks made like this. prefixed(translatable() @@ -705,7 +707,7 @@ static TextComponent prefixed(ComponentLike component) { .args(translatable("luckperms.command.verbose.disabled-term", RED)) ); - Args1 VERBOSE_RESULTS_URL = url -> join(newline(), + Args1 VERBOSE_RESULTS_URL = url -> joinNewline( // "&aVerbose results URL:" // prefixed(translatable() @@ -731,7 +733,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args1 TREE_URL = url -> join(newline(), + Args1 TREE_URL = url -> joinNewline( // "&aPermission tree URL:" // prefixed(translatable() @@ -886,7 +888,7 @@ static TextComponent prefixed(ComponentLike component) { .apply(builder -> { boolean explicitGlobalContext = !plugin.getConfiguration().getContextsFile().getDefaultContexts().isEmpty(); - Component hover = join(newline(), + Component hover = joinNewline( text() .append(text('>', DARK_AQUA)) .append(space()) @@ -930,7 +932,7 @@ static TextComponent prefixed(ComponentLike component) { .apply(builder -> { boolean explicitGlobalContext = !plugin.getConfiguration().getContextsFile().getDefaultContexts().isEmpty(); - Component hover = join(newline(), + Component hover = joinNewline( text() .append(text('>', DARK_AQUA)) .append(space()) @@ -949,7 +951,7 @@ static TextComponent prefixed(ComponentLike component) { }) .build(); - Args2 APPLY_EDITS_SESSION_UNKNOWN = (code, label) -> join(newline(), + Args2 APPLY_EDITS_SESSION_UNKNOWN = (code, label) -> joinNewline( // "&4The changes received from the web editor were not made in a session started on this server!" // "&cAre you sure you're running the /lp applyedits command in the right place?" // "&cTo ignore this warning and apply the changes anyway, run: &4/lp applyedits --force" @@ -967,7 +969,7 @@ static TextComponent prefixed(ComponentLike component) { .append(text("/" + label + " applyedits " + code + " --force", DARK_RED))) ); - Args2 APPLY_EDITS_SESSION_APPLIED_ALREADY = (code, label) -> join(newline(), + Args2 APPLY_EDITS_SESSION_APPLIED_ALREADY = (code, label) -> joinNewline( // "&4The changes received from the web editor are based on an initial session which has already been applied!" // "&cTo avoid conflicts, you should never re-use the same editor session after the changes from it have been applied once already." // "&cTo ignore this warning and apply the changes anyway, run: /lp applyedits --force" @@ -1149,7 +1151,7 @@ static TextComponent prefixed(ComponentLike component) { .key("luckperms.command.editor.start") ); - Args1 EDITOR_URL = url -> join(newline(), + Args1 EDITOR_URL = url -> joinNewline( // "&aClick the link below to open the editor:" // prefixed(translatable() @@ -1183,7 +1185,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args4 EDITOR_SOCKET_UNTRUSTED = (nonce, browser, cmdLabel, console) -> join(newline(), + Args4 EDITOR_SOCKET_UNTRUSTED = (nonce, browser, cmdLabel, console) -> joinNewline( // "&bAn editor window has connected, but it is not yet trusted." // "&8(&7session id = &faaaaa&7, browser = &fChrome on Windows 10&8)" // "&7If it was you, &aclick here&7 to trust the session!" @@ -1229,7 +1231,7 @@ static TextComponent prefixed(ComponentLike component) { })) ); - Args0 EDITOR_SOCKET_TRUST_SUCCESS = () -> join(newline(), + Args0 EDITOR_SOCKET_TRUST_SUCCESS = () -> joinNewline( // "&aThe editor session has been marked as trusted." // "&7In the future, connections from the same browser will be trusted automatically." // "&7The plugin will now attempt to establish a connection with the editor..." @@ -1533,7 +1535,7 @@ static TextComponent prefixed(ComponentLike component) { ) ); - Args2> INFO = (plugin, storageMeta) -> join(newline(), + Args2> INFO = (plugin, storageMeta) -> joinNewline( // "&2Running &bLuckPerms v{}&2 by &bLuck&2." // "&f- &3Platform: &f{}" // "&f- &3Server Brand: &f{}" @@ -1768,7 +1770,7 @@ static TextComponent prefixed(ComponentLike component) { String holderName = holder.getType() == HolderType.GROUP ? holder.getIdentifier().getName() : holder.getPlainDisplayName(); boolean explicitGlobalContext = !holder.getPlugin().getConfiguration().getContextsFile().getDefaultContexts().isEmpty(); - Component hover = join(newline(), + Component hover = joinNewline( text() .append(text('>', DARK_AQUA)) .append(space()) @@ -1787,7 +1789,7 @@ static TextComponent prefixed(ComponentLike component) { }) .build(); - Args3 PERMISSION_INFO_TEMPORARY_NODE_ENTRY = (node, holder, label) -> join(newline(), + Args3 PERMISSION_INFO_TEMPORARY_NODE_ENTRY = (node, holder, label) -> joinNewline( text() .append(text('>', DARK_AQUA)) .append(space()) @@ -1798,7 +1800,7 @@ static TextComponent prefixed(ComponentLike component) { String holderName = holder.getType() == HolderType.GROUP ? holder.getIdentifier().getName() : holder.getPlainDisplayName(); boolean explicitGlobalContext = !holder.getPlugin().getConfiguration().getContextsFile().getDefaultContexts().isEmpty(); - Component hover = join(newline(), + Component hover = joinNewline( text() .append(text('>', DARK_AQUA)) .append(space()) @@ -1868,7 +1870,7 @@ static TextComponent prefixed(ComponentLike component) { String holderName = holder.getType() == HolderType.GROUP ? holder.getIdentifier().getName() : holder.getPlainDisplayName(); boolean explicitGlobalContext = !holder.getPlugin().getConfiguration().getContextsFile().getDefaultContexts().isEmpty(); - Component hover = join(newline(), + Component hover = joinNewline( text() .append(text('>', DARK_AQUA)) .append(space()) @@ -1890,7 +1892,7 @@ static TextComponent prefixed(ComponentLike component) { .append(formatContextSetBracketed(node.getContexts(), empty())) .build(); - Args3 PARENT_INFO_TEMPORARY_NODE_ENTRY = (node, holder, label) -> join(newline(), + Args3 PARENT_INFO_TEMPORARY_NODE_ENTRY = (node, holder, label) -> joinNewline( text() .append(text('>', DARK_AQUA)) .append(space()) @@ -1901,7 +1903,7 @@ static TextComponent prefixed(ComponentLike component) { String holderName = holder.getType() == HolderType.GROUP ? holder.getIdentifier().getName() : holder.getPlainDisplayName(); boolean explicitGlobalContext = !holder.getPlugin().getConfiguration().getContextsFile().getDefaultContexts().isEmpty(); - Component hover = join(newline(), + Component hover = joinNewline( text() .append(text('>', DARK_AQUA)) .append(text(node.getGroupName(), WHITE)), @@ -1938,7 +1940,7 @@ static TextComponent prefixed(ComponentLike component) { .append(text(':')) ); - Args3 LIST_TRACKS_ENTRY = (name, contextSet, path) -> join(newline(), + Args3 LIST_TRACKS_ENTRY = (name, contextSet, path) -> joinNewline( // "&3> &a{}: {}" // "&7 ({}&7)" text() @@ -2040,7 +2042,7 @@ static TextComponent prefixed(ComponentLike component) { ) ); - Args5 PERMISSION_CHECK_RESULT = (permission, result, processor, causeNode, context) -> join(newline(), + Args5 PERMISSION_CHECK_RESULT = (permission, result, processor, causeNode, context) -> joinNewline( // &aPermission check for &b{}&a: // &3Result: {} // &3Processor: &f{} @@ -2516,7 +2518,7 @@ static TextComponent prefixed(ComponentLike component) { HolderType originType = HolderType.valueOf(origin.getOrigin().getType().toUpperCase(Locale.ROOT)); boolean explicitGlobalContext = !holder.getPlugin().getConfiguration().getContextsFile().getDefaultContexts().isEmpty(); - Component hover = join(newline(), + Component hover = joinNewline( text() .append(text('>', DARK_AQUA)) .append(space()) @@ -2591,7 +2593,7 @@ static TextComponent prefixed(ComponentLike component) { HolderType originType = HolderType.valueOf(origin.getOrigin().getType().toUpperCase(Locale.ROOT)); boolean explicitGlobalContext = !holder.getPlugin().getConfiguration().getContextsFile().getDefaultContexts().isEmpty(); - Component hover = join(newline(), + Component hover = joinNewline( text() .append(text('>', DARK_AQUA)) .append(space()) @@ -2987,7 +2989,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args3 BULK_UPDATE_STATISTICS = (nodes, users, groups) -> join(newline(), + Args3 BULK_UPDATE_STATISTICS = (nodes, users, groups) -> joinNewline( // "&bTotal affected nodes: &a{}" // "&bTotal affected users: &a{}" // "&bTotal affected groups: &a{}" @@ -3069,7 +3071,7 @@ static TextComponent prefixed(ComponentLike component) { }) ); - Args1 TRANSLATIONS_DOWNLOAD_PROMPT = label -> join(newline(), + Args1 TRANSLATIONS_DOWNLOAD_PROMPT = label -> joinNewline( // "Use /lp translations install to download and install up-to-date versions of these translations provided by the community." // "Please note that this will override any changes you've made for these languages." prefixed(translatable() @@ -3113,7 +3115,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args4 USER_INFO_GENERAL = (username, uuid, uuidType, online) -> join(newline(), + Args4 USER_INFO_GENERAL = (username, uuid, uuidType, online) -> joinNewline( // "&b&l> &bUser Info: &f{}" // "&f- &3UUID: &f{}" // "&f &7(type: {}&7)" @@ -3147,7 +3149,7 @@ static TextComponent prefixed(ComponentLike component) { .append(online ? translatable("luckperms.command.user.info.status.online", GREEN) : translatable("luckperms.command.user.info.status.offline", RED))) ); - Args6>> USER_INFO_CONTEXTUAL_DATA = (active, contexts, prefix, suffix, primaryGroup, meta) -> join(newline(), + Args6>> USER_INFO_CONTEXTUAL_DATA = (active, contexts, prefix, suffix, primaryGroup, meta) -> joinNewline( // "&f- &aContextual Data: &7(mode: {}&7)" // " &3Contexts: {}" // " &3Prefix: {}" @@ -3234,7 +3236,7 @@ static TextComponent prefixed(ComponentLike component) { .build() ) .collect(Collectors.toList()); - builder.append(join(space(), entries)); + builder.append(join(JoinConfiguration.separator(space()), entries)); } })) ); @@ -3261,7 +3263,7 @@ static TextComponent prefixed(ComponentLike component) { .append(formatContextSetBracketed(node.getContexts(), empty())) ); - Args1 INFO_PARENT_TEMPORARY_NODE_ENTRY = node -> join(newline(), + Args1 INFO_PARENT_TEMPORARY_NODE_ENTRY = node -> joinNewline( prefixed(text() .append(text(" > ", DARK_AQUA)) .append(text(node.getGroupName(), WHITE)) @@ -3390,7 +3392,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args1 USER_PROMOTE_ERROR_MALFORMED = name -> join(newline(), + Args1 USER_PROMOTE_ERROR_MALFORMED = name -> joinNewline( // "&aThe next group on the track, &b{}&a, no longer exists. Unable to promote user." // "&aEither create the group, or remove it from the track and try again." prefixed(translatable() @@ -3444,7 +3446,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args1 USER_DEMOTE_ERROR_MALFORMED = name -> join(newline(), + Args1 USER_DEMOTE_ERROR_MALFORMED = name -> joinNewline( // "&aThe previous group on the track, &b{}&a, no longer exists. Unable to demote user." // "&aEither create the group, or remove it from the track and try again." prefixed(translatable() @@ -3461,7 +3463,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP)) ); - Args3 GROUP_INFO_GENERAL = (name, displayName, weight) -> join(newline(), + Args3 GROUP_INFO_GENERAL = (name, displayName, weight) -> joinNewline( // "&b&l> &bGroup Info: &f{}" // "&f- &3Display Name: &f{}" // "&f- &3Weight: &f{}" @@ -3486,7 +3488,7 @@ static TextComponent prefixed(ComponentLike component) { .append(weight.isPresent() ? text(weight.getAsInt(), WHITE) : translatable("luckperms.command.generic.contextual-data.null-result", WHITE))) ); - Args3>> GROUP_INFO_CONTEXTUAL_DATA = (prefix, suffix, meta) -> join(newline(), + Args3>> GROUP_INFO_CONTEXTUAL_DATA = (prefix, suffix, meta) -> joinNewline( // "&f- &aContextual Data: &7(mode: &8server&7)" // " &3Prefix: {}" // " &3Suffix: {}" @@ -3559,7 +3561,7 @@ static TextComponent prefixed(ComponentLike component) { .build() ) .collect(Collectors.toList()); - builder.append(join(space(), entries)); + builder.append(join(JoinConfiguration.separator(space()), entries)); } })) ); @@ -3615,7 +3617,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args2 TRACK_INFO = (name, path) -> join(newline(), + Args2 TRACK_INFO = (name, path) -> joinNewline( // "&b&l> &bShowing Track: &f{}" + "\n" + // "&f- &7Path: &f{}", prefixed(text() @@ -3721,7 +3723,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args2 LOG_ENTRY = (pos, action) -> join(newline(), + Args2 LOG_ENTRY = (pos, action) -> joinNewline( // "&b#{} &8(&7{} ago&8) &8(&e{}&8) [&a{}&8] (&b{}&8)" // "&7> &f{}" prefixed(text() @@ -3982,7 +3984,7 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args2 EXPORT_WEB_SUCCESS = (pasteId, label) -> join(newline(), + Args2 EXPORT_WEB_SUCCESS = (pasteId, label) -> joinNewline( // "&aExport code: &7{}" // "&7Use the following command to import:" // "&a/{} import {} --upload" @@ -3995,7 +3997,8 @@ static TextComponent prefixed(ComponentLike component) { .key("luckperms.command.export.web.import-command-description") .color(GRAY) .append(text(":")), - text("/" + label + " import " + pasteId + " --upload", GREEN)); + text("/" + label + " import " + pasteId + " --upload", GREEN) + ); Args1 IMPORT_FILE_DOESNT_EXIST = file -> prefixed(text() // "&cError: File &4{}&c does not exist." @@ -4150,9 +4153,18 @@ static TextComponent prefixed(ComponentLike component) { ); static Component formatColoredValue(String value) { - return LegacyComponentSerializer.legacyAmpersand().deserialize(value).toBuilder() - .hoverEvent(HoverEvent.showText(text(value, WHITE))) - .build(); + boolean containsLegacyFormattingCharacter = value.indexOf(LegacyComponentSerializer.AMPERSAND_CHAR) != 1 + || value.indexOf(LegacyComponentSerializer.SECTION_CHAR) != 1; + + HoverEvent hover = HoverEvent.showText(text(value, WHITE)); + + if (containsLegacyFormattingCharacter) { + return LegacyComponentSerializer.legacyAmpersand().deserialize(value).toBuilder() + .hoverEvent(hover) + .build(); + } else { + return MiniMessage.miniMessage().deserialize(value).hoverEvent(hover); + } } static Component formatContextBracketed(String key, String value) { @@ -4320,6 +4332,10 @@ static Component formatTristate(Tristate tristate) { } } + static Component joinNewline(final ComponentLike... components) { + return join(JoinConfiguration.newlines(), components); + } + interface Args0 { Component build(); diff --git a/standalone/app/build.gradle b/standalone/app/build.gradle index aa2ab7bb8..ee7edfdb9 100644 --- a/standalone/app/build.gradle +++ b/standalone/app/build.gradle @@ -36,6 +36,10 @@ dependencies { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') } + api('net.kyori:adventure-text-minimessage:4.14.0') { + exclude(module: 'adventure-bom') + exclude(module: 'adventure-api') + } api('net.kyori:ansi:1.0.1') } From 7297fb862caadeb1ca4561bb593197cfabd4036b Mon Sep 17 00:00:00 2001 From: powercas_gamer Date: Sat, 24 Jun 2023 10:12:06 +0200 Subject: [PATCH 15/23] whoops (#3691) --- .../main/java/me/lucko/luckperms/common/locale/Message.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java index 3a6ea5f56..710d86bf7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/Message.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/Message.java @@ -4153,8 +4153,8 @@ static TextComponent prefixed(ComponentLike component) { ); static Component formatColoredValue(String value) { - boolean containsLegacyFormattingCharacter = value.indexOf(LegacyComponentSerializer.AMPERSAND_CHAR) != 1 - || value.indexOf(LegacyComponentSerializer.SECTION_CHAR) != 1; + boolean containsLegacyFormattingCharacter = value.indexOf(LegacyComponentSerializer.AMPERSAND_CHAR) != -1 + || value.indexOf(LegacyComponentSerializer.SECTION_CHAR) != -1; HoverEvent hover = HoverEvent.showText(text(value, WHITE)); From f788088a69e4acac230702f496f1e7bc2788910f Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 25 Jun 2023 14:26:06 +0100 Subject: [PATCH 16/23] Delete temporary H2 v1 to v2 migration script if exists (#3693) --- .../luckperms/common/messaging/redis/RedisMessenger.java | 3 --- .../me/lucko/luckperms/common/sender/AbstractSender.java | 7 ++++++- .../sql/connection/file/H2ConnectionFactory.java | 7 +++++++ .../sql/connection/hikari/MariaDbConnectionFactory.java | 2 -- .../sql/connection/hikari/MySqlConnectionFactory.java | 4 ---- .../luckperms/standalone/CommandsIntegrationTest.java | 4 +++- .../luckperms/standalone/ImportExportIntegrationTest.java | 4 +++- .../me/lucko/luckperms/standalone/IntegrationTest.java | 4 +++- .../lucko/luckperms/standalone/utils/CommandTester.java | 8 +++++++- 9 files changed, 29 insertions(+), 14 deletions(-) diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java index e9e5ff69d..cfe9ac5a5 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/redis/RedisMessenger.java @@ -34,15 +34,12 @@ import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisClientConfig; import redis.clients.jedis.JedisCluster; -import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPooled; import redis.clients.jedis.JedisPubSub; import redis.clients.jedis.Protocol; import redis.clients.jedis.UnifiedJedis; -import redis.clients.jedis.exceptions.JedisClusterOperationException; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; diff --git a/common/src/main/java/me/lucko/luckperms/common/sender/AbstractSender.java b/common/src/main/java/me/lucko/luckperms/common/sender/AbstractSender.java index 0a72cd950..9d12ca53d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/sender/AbstractSender.java +++ b/common/src/main/java/me/lucko/luckperms/common/sender/AbstractSender.java @@ -31,7 +31,12 @@ import net.kyori.adventure.text.TextComponent; import net.luckperms.api.util.Tristate; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; /** * Simple implementation of {@link Sender} using a {@link SenderFactory} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java index b0c7808b2..cc36994ed 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/H2ConnectionFactory.java @@ -28,6 +28,7 @@ import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import java.io.IOException; import java.lang.reflect.Constructor; import java.nio.file.Files; import java.nio.file.Path; @@ -123,6 +124,12 @@ public void run(H2ConnectionFactory newFactory) throws Exception { this.plugin.getLogger().warn("[DB Upgrade] Found an old (v1) H2 database file. LuckPerms will now attempt to upgrade it to v2 (this is a one time operation)."); + try { + Files.deleteIfExists(tempMigrationFile); + } catch (IOException e) { + this.plugin.getLogger().warn("[DB Upgrade] Unable to delete temporary data from a previous migration attempt", e); + } + this.plugin.getLogger().info("[DB Upgrade] Stage 1: Exporting the old database to an intermediary file..."); Constructor constructor = getConnectionConstructor(); try (Connection c = getConnection(constructor, oldDatabase)) { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/MariaDbConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/MariaDbConnectionFactory.java index b41c203a6..142b0fc93 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/MariaDbConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/MariaDbConnectionFactory.java @@ -28,9 +28,7 @@ import com.zaxxer.hikari.HikariConfig; import me.lucko.luckperms.common.storage.misc.StorageCredentials; -import java.util.Map; import java.util.function.Function; -import java.util.stream.Collectors; public class MariaDbConnectionFactory extends HikariConnectionFactory { public MariaDbConnectionFactory(StorageCredentials configuration) { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/MySqlConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/MySqlConnectionFactory.java index ce96742c5..81e1e989f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/MySqlConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/MySqlConnectionFactory.java @@ -28,10 +28,6 @@ import com.zaxxer.hikari.HikariConfig; import me.lucko.luckperms.common.storage.misc.StorageCredentials; -import java.sql.Driver; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Enumeration; import java.util.Map; import java.util.function.Function; diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java index 9e29e3736..bd3e6f73e 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java @@ -46,7 +46,9 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class CommandsIntegrationTest { diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java index 0da5ee7e3..2600033d5 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java @@ -51,7 +51,9 @@ import java.util.zip.GZIPInputStream; import static org.awaitility.Awaitility.await; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; public class ImportExportIntegrationTest { diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java index cde973c4d..278dacb58 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java @@ -40,7 +40,9 @@ import java.nio.file.Files; import java.nio.file.Path; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * A set of 'integration tests' for the standalone LuckPerms app. diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java index d3ebf27be..bd6e16e93 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java @@ -35,7 +35,13 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; From 77a5a7b41f5896e3f32c6fb858d71bae530e6591 Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 25 Jun 2023 14:47:37 +0100 Subject: [PATCH 17/23] Print test output when ran through Gradle --- build.gradle | 15 ++++++++++++++- .../standalone/CommandsIntegrationTest.java | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index cf81bd37e..c8431de6a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,9 @@ plugins { alias(libs.plugins.licenser) apply false } +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + defaultTasks 'licenseFormat', 'build' subprojects { @@ -15,10 +18,20 @@ subprojects { sourceCompatibility = 1.8 targetCompatibility = 1.8 - tasks.withType(JavaCompile) { + tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' } + tasks.withType(Test).configureEach { + testLogging { + events = [TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.SKIPPED] + exceptionFormat = TestExceptionFormat.FULL + showExceptions = true + showCauses = true + showStackTraces = true + } + } + jar { from '../LICENSE.txt' } diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java index bd3e6f73e..419960465 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java @@ -1088,7 +1088,7 @@ public void testBulkUpdate(@TempDir Path tempDir) throws InterruptedException { .whenRunCommand("bulkupdate all update permission group.mod \"permission == group.moderator\"") .thenExpect("[LP] Running bulk update."); - assertTrue(completed.await(5, TimeUnit.SECONDS)); + assertTrue(completed.await(15, TimeUnit.SECONDS), "operation did not complete in the allotted time"); Group adminGroup = plugin.getGroupManager().getIfLoaded("admin"); assertNotNull(adminGroup); From 9f4082ca0b9a8be7e52cfd56ca2619ef88024211 Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 25 Jun 2023 22:25:42 +0100 Subject: [PATCH 18/23] Add more unit/integration tests --- .../common/cache/BufferedRequest.java | 11 ++ .../commands/generic/other/HolderEditor.java | 2 +- .../common/commands/misc/EditorCommand.java | 2 +- .../common/commands/track/TrackEditor.java | 2 +- .../luckperms/common/config/ConfigKeys.java | 5 + .../common/http/BytesocksClient.java | 6 +- .../plugin/AbstractLuckPermsPlugin.java | 17 +- .../implementation/file/FileActionLogger.java | 5 + .../common/storage/misc/NodeEntry.java | 5 + .../common/webeditor/WebEditorSession.java | 19 +- .../webeditor/store/WebEditorStore.java | 3 - .../command/utils/ArgumentListTest.java | 105 +++++++++++ .../command/utils/ArgumentTokenizerTest.java | 92 ++++++++++ standalone/build.gradle | 3 +- .../standalone/StorageIntegrationTest.java | 79 +++++++-- .../standalone/WebEditorIntegrationTest.java | 164 ++++++++++++++++++ .../standalone/utils/TestPluginBootstrap.java | 2 +- .../example/hocon-combined/tracks.conf | 7 +- .../example/hocon/tracks/example.conf | 5 + .../example/json-combined/tracks.json | 9 +- .../example/json/tracks/example.json | 7 + .../example/toml-combined/groups.toml | 33 ---- .../example/toml-combined/tracks.toml | 0 .../example/toml-combined/users.toml | 42 ----- .../example/toml/groups/default.toml | 1 - .../resources/example/toml/groups/test.toml | 37 ---- .../c1d60c50-70b5-4722-8057-87767557e50d.toml | 42 ----- .../example/yaml-combined/tracks.yml | 5 +- .../resources/example/yaml/tracks/example.yml | 4 + 29 files changed, 523 insertions(+), 191 deletions(-) create mode 100644 common/src/test/java/me/lucko/luckperms/common/command/utils/ArgumentListTest.java create mode 100644 common/src/test/java/me/lucko/luckperms/common/command/utils/ArgumentTokenizerTest.java create mode 100644 standalone/src/test/java/me/lucko/luckperms/standalone/WebEditorIntegrationTest.java create mode 100644 standalone/src/test/resources/example/hocon/tracks/example.conf create mode 100644 standalone/src/test/resources/example/json/tracks/example.json delete mode 100644 standalone/src/test/resources/example/toml-combined/groups.toml delete mode 100644 standalone/src/test/resources/example/toml-combined/tracks.toml delete mode 100644 standalone/src/test/resources/example/toml-combined/users.toml delete mode 100644 standalone/src/test/resources/example/toml/groups/default.toml delete mode 100644 standalone/src/test/resources/example/toml/groups/test.toml delete mode 100644 standalone/src/test/resources/example/toml/users/c1d60c50-70b5-4722-8057-87767557e50d.toml create mode 100644 standalone/src/test/resources/example/yaml/tracks/example.yml diff --git a/common/src/main/java/me/lucko/luckperms/common/cache/BufferedRequest.java b/common/src/main/java/me/lucko/luckperms/common/cache/BufferedRequest.java index 93d938843..6189c03de 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cache/BufferedRequest.java +++ b/common/src/main/java/me/lucko/luckperms/common/cache/BufferedRequest.java @@ -86,6 +86,17 @@ public CompletableFuture request() { } } + /** + * Gets if the request buffer has been enqueued + * + * @return if the buffer is enqueued + */ + public boolean isEnqueued() { + synchronized (this.mutex) { + return this.processor != null; + } + } + /** * Requests the value, bypassing the buffer * diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderEditor.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderEditor.java index 25044ae6b..656f2d019 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderEditor.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderEditor.java @@ -84,7 +84,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, T target, ArgumentLis Message.EDITOR_START.send(sender); - WebEditorSession.createAndOpen(holders, Collections.emptyList(), sender, label, plugin); + WebEditorSession.create(holders, Collections.emptyList(), sender, label, plugin).open(); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/EditorCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/EditorCommand.java index 46b182dfb..5f55c03ce 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/EditorCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/EditorCommand.java @@ -107,7 +107,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St Message.EDITOR_START.send(sender); - WebEditorSession.createAndOpen(holders, tracks, sender, label, plugin); + WebEditorSession.create(holders, tracks, sender, label, plugin).open(); } private enum Type { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackEditor.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackEditor.java index cc4efff64..61856d766 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackEditor.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackEditor.java @@ -97,7 +97,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Track target, Argumen Message.EDITOR_START.send(sender); - WebEditorSession.createAndOpen(holders, Collections.singletonList(target), sender, label, plugin); + WebEditorSession.create(holders, Collections.singletonList(target), sender, label, plugin).open(); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java index dcdea307c..216915694 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java @@ -725,6 +725,11 @@ private ConfigKeys() {} */ public static final ConfigKey BYTESOCKS_HOST = stringKey("bytesocks-host", "usersockets.luckperms.net"); + /** + * If TLS (https/wss) should be used when connecting to bytesocks + */ + public static final ConfigKey BYTESOCKS_USE_TLS = booleanKey("bytesocks-use-tls", true); + /** * The URL of the web editor */ diff --git a/common/src/main/java/me/lucko/luckperms/common/http/BytesocksClient.java b/common/src/main/java/me/lucko/luckperms/common/http/BytesocksClient.java index 61affe512..b8cb50958 100644 --- a/common/src/main/java/me/lucko/luckperms/common/http/BytesocksClient.java +++ b/common/src/main/java/me/lucko/luckperms/common/http/BytesocksClient.java @@ -49,11 +49,11 @@ public class BytesocksClient extends AbstractHttpClient { * @param host the bytesocks host * @param userAgent the client user agent string */ - public BytesocksClient(OkHttpClient okHttpClient, String host, String userAgent) { + public BytesocksClient(OkHttpClient okHttpClient, String host, boolean tls, String userAgent) { super(okHttpClient); - this.httpUrl = "https://" + host + "/"; - this.wsUrl = "wss://" + host + "/"; + this.httpUrl = (tls ? "https://" : "http://") + host + "/"; + this.wsUrl = (tls ? "wss://" : "ws://") + host + "/"; this.userAgent = userAgent; } diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java index fed88bb39..fc662324e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java @@ -148,8 +148,17 @@ public final void enable() { .callTimeout(15, TimeUnit.SECONDS) .build(); - this.bytebin = new BytebinClient(this.httpClient, getConfiguration().get(ConfigKeys.BYTEBIN_URL), "luckperms"); - this.bytesocks = new BytesocksClient(this.httpClient, getConfiguration().get(ConfigKeys.BYTESOCKS_HOST), "luckperms/editor"); + this.bytebin = new BytebinClient( + this.httpClient, + getConfiguration().get(ConfigKeys.BYTEBIN_URL), + "luckperms" + ); + this.bytesocks = new BytesocksClient( + this.httpClient, + getConfiguration().get(ConfigKeys.BYTESOCKS_HOST), + getConfiguration().get(ConfigKeys.BYTESOCKS_USE_TLS), + "luckperms/editor" + ); this.webEditorStore = new WebEditorStore(this); // init translation repo and update bundle files @@ -451,6 +460,10 @@ public LuckPermsConfiguration getConfiguration() { return this.configuration; } + public OkHttpClient getHttpClient() { + return this.httpClient; + } + @Override public BytebinClient getBytebin() { return this.bytebin; diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileActionLogger.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileActionLogger.java index 12a15d375..263034507 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileActionLogger.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileActionLogger.java @@ -131,6 +131,11 @@ public void flush() { } public Log getLog() throws IOException { + // if there is log content waiting to be written, flush immediately before trying to read + if (this.saveBuffer.isEnqueued()) { + this.saveBuffer.requestDirectly(); + } + if (!Files.exists(this.contentFile)) { return Log.empty(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/misc/NodeEntry.java b/common/src/main/java/me/lucko/luckperms/common/storage/misc/NodeEntry.java index 6c51df421..6b8ad0b4e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/misc/NodeEntry.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/misc/NodeEntry.java @@ -54,6 +54,11 @@ private NodeEntry(H holder, N node) { return this.node; } + @Override + public String toString() { + return "NodeEntry(holder=" + this.holder + ", node=" + this.node + ')'; + } + @Override public boolean equals(Object o) { if (o == this) return true; diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorSession.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorSession.java index 137558518..67d5c8f39 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorSession.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorSession.java @@ -56,10 +56,9 @@ */ public class WebEditorSession { - public static void createAndOpen(List holders, List tracks, Sender sender, String cmdLabel, LuckPermsPlugin plugin) { + public static WebEditorSession create(List holders, List tracks, Sender sender, String cmdLabel, LuckPermsPlugin plugin) { WebEditorRequest initialRequest = WebEditorRequest.generate(holders, tracks, sender, cmdLabel, plugin); - WebEditorSession session = new WebEditorSession(initialRequest, plugin, sender, cmdLabel); - session.open(); + return new WebEditorSession(initialRequest, plugin, sender, cmdLabel); } private WebEditorRequest initialRequest; @@ -83,9 +82,9 @@ public WebEditorSession(WebEditorRequest initialRequest, LuckPermsPlugin plugin, this.tracks = new LinkedHashSet<>(initialRequest.getTracks().keySet()); } - public void open() { + public String open() { createSocket(); - createInitialSession(); + return createInitialSession(); } private void createSocket() { @@ -117,7 +116,7 @@ private static boolean ignoreSocketConnectError(Exception e) { return false; } - private void createInitialSession() { + private String createInitialSession() { Objects.requireNonNull(this.initialRequest); WebEditorRequest request = this.initialRequest; @@ -129,7 +128,7 @@ private void createInitialSession() { String id = uploadRequestData(request); if (id == null) { - return; + return null; } // form a url for the editor @@ -140,6 +139,12 @@ private void createInitialSession() { if (this.socket != null) { this.socket.scheduleCleanupIfUnused(); } + + return id; + } + + public WebEditorSocket getSocket() { + return this.socket; } public void includeCreatedGroup(Group group) { diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java index e92c51b0e..7e806d8ac 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java @@ -76,9 +76,6 @@ public WebEditorKeystore keystore() { } public KeyPair keyPair() { - if (!this.keyPair.get().isDone()) { - throw new IllegalStateException("Web editor keypair has not been generated yet! Has the server just started?"); - } return this.keyPair.get().join(); } diff --git a/common/src/test/java/me/lucko/luckperms/common/command/utils/ArgumentListTest.java b/common/src/test/java/me/lucko/luckperms/common/command/utils/ArgumentListTest.java new file mode 100644 index 000000000..1b0c85810 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/command/utils/ArgumentListTest.java @@ -0,0 +1,105 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.command.utils; + +import com.google.common.collect.ImmutableList; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import net.luckperms.api.context.ContextSet; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ArgumentListTest { + + @Test + public void testGetString() { + ArgumentList list = new ArgumentList(ImmutableList.of("hello", "world{SPACE}")); + + assertEquals("hello", list.getOrDefault(0, "def")); + assertEquals("world ", list.getOrDefault(1, "def")); + assertEquals("def", list.getOrDefault(2, "def")); + assertEquals("def", list.getOrDefault(-1, "def")); + assertNull(list.getOrDefault(2, null)); + assertNull(list.getOrDefault(-1, null)); + } + + @Test + public void testGetInt() { + ArgumentList list = new ArgumentList(ImmutableList.of("5", "-50")); + + assertEquals(5, list.getIntOrDefault(0, -1)); + assertEquals(-50, list.getIntOrDefault(1, -1)); + assertEquals(-1, list.getIntOrDefault(2, -1)); + assertEquals(-1, list.getIntOrDefault(-1, -1)); + } + + private static Stream testParseContext() { + return Stream.of( + Arguments.of(new String[]{}, ImmutableContextSetImpl.EMPTY), + Arguments.of(new String[]{"test"}, ImmutableContextSetImpl.of("server", "test")), + Arguments.of( + new String[]{"a", "b", "c"}, + new ImmutableContextSetImpl.BuilderImpl() + .add("server", "a") + .add("world", "b") + .add("server", "c") + .build() + ), + Arguments.of( + new String[]{"a", "thing=b", "c"}, + new ImmutableContextSetImpl.BuilderImpl() + .add("server", "a") + .add("thing", "b") + .add("server", "c") + .build() + ), + Arguments.of( + new String[]{"thing=a", "thing=b", "c"}, + new ImmutableContextSetImpl.BuilderImpl() + .add("thing", "a") + .add("thing", "b") + .add("server", "c") + .build() + ), + Arguments.of(new String[]{"="}, ImmutableContextSetImpl.EMPTY) + ); + } + + @ParameterizedTest + @MethodSource + public void testParseContext(String[] arguments, ContextSet expected) { + ArgumentList list = new ArgumentList(ImmutableList.copyOf(arguments)); + assertEquals(expected, list.getContextOrEmpty(0)); + } + + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/command/utils/ArgumentTokenizerTest.java b/common/src/test/java/me/lucko/luckperms/common/command/utils/ArgumentTokenizerTest.java new file mode 100644 index 000000000..2d91281ac --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/command/utils/ArgumentTokenizerTest.java @@ -0,0 +1,92 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.command.utils; + +import com.google.common.collect.ImmutableList; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ArgumentTokenizerTest { + + private static Stream testBasicTokenize() { + return Stream.of( + Arguments.of("", new String[]{}), + Arguments.of("hello world", new String[]{"hello", "world"}), + Arguments.of("hello world", new String[]{"hello", "", "world"}), + Arguments.of("hello world", new String[]{"hello", "", "", "world"}), + Arguments.of("\"hello world\"", new String[]{"hello world"}), + Arguments.of("\"hello world\"", new String[]{"hello world"}), + Arguments.of("\" hello world\"", new String[]{" hello world"}), + Arguments.of("\"hello world \"", new String[]{"hello world "}), + Arguments.of("\"hello\"\"world\"", new String[]{"hello", "world"}), + Arguments.of("\"hello\" \"world\"", new String[]{"hello", "world"}) + ); + } + + @ParameterizedTest + @MethodSource + public void testBasicTokenize(String input, String[] expectedTokens) { + for (ArgumentTokenizer tokenizer : ArgumentTokenizer.values()) { + List tokens = tokenizer.tokenizeInput(input); + assertEquals(ImmutableList.copyOf(expectedTokens), ImmutableList.copyOf(tokens), "tokenizer " + tokenizer + " produced tokens " + tokens); + } + } + + private static Stream testExecuteTokenize() { + return Stream.of( + Arguments.of("hello world ", new String[]{"hello", "world"}), + Arguments.of("hello world ", new String[]{"hello", "world", ""}) + ); + } + + @ParameterizedTest + @MethodSource + public void testExecuteTokenize(String input, String[] expectedTokens) { + List tokens = ArgumentTokenizer.EXECUTE.tokenizeInput(input); + assertEquals(ImmutableList.copyOf(expectedTokens), ImmutableList.copyOf(tokens)); + } + + private static Stream testTabCompleteTokenize() { + return Stream.of( + Arguments.of("hello world ", new String[]{"hello", "world", ""}), + Arguments.of("hello world ", new String[]{"hello", "world", "", ""}) + ); + } + + @ParameterizedTest + @MethodSource + public void testTabCompleteTokenize(String input, String[] expectedTokens) { + List tokens = ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(input); + assertEquals(ImmutableList.copyOf(expectedTokens), ImmutableList.copyOf(tokens)); + } + +} diff --git a/standalone/build.gradle b/standalone/build.gradle index 6713ddb9f..aa4023596 100644 --- a/standalone/build.gradle +++ b/standalone/build.gradle @@ -23,7 +23,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.1' - testImplementation "org.testcontainers:junit-jupiter:1.17.6" + testImplementation "org.testcontainers:junit-jupiter:1.18.3" testImplementation 'org.mockito:mockito-core:4.11.0' testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0' testImplementation 'org.awaitility:awaitility:4.2.0' @@ -34,6 +34,7 @@ dependencies { testImplementation 'com.rabbitmq:amqp-client:5.12.0' testImplementation 'org.postgresql:postgresql:42.6.0' testImplementation 'com.h2database:h2:2.1.214' + testImplementation 'org.xerial:sqlite-jdbc:3.42.0.0' testImplementation 'mysql:mysql-connector-java:8.0.23' testImplementation 'org.mariadb.jdbc:mariadb-java-client:3.1.3' testImplementation 'org.mongodb:mongodb-driver-legacy:4.5.0' diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java index 9a198c7ff..0592c2532 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java @@ -25,22 +25,31 @@ package me.lucko.luckperms.standalone; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.node.matcher.StandardNodeMatchers; import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.node.types.Meta; import me.lucko.luckperms.common.node.types.Permission; import me.lucko.luckperms.common.node.types.Prefix; +import me.lucko.luckperms.common.storage.misc.NodeEntry; import me.lucko.luckperms.standalone.app.LuckPermsApplication; import me.lucko.luckperms.standalone.app.integration.HealthReporter; import me.lucko.luckperms.standalone.utils.TestPluginBootstrap; import me.lucko.luckperms.standalone.utils.TestPluginBootstrap.TestPlugin; import me.lucko.luckperms.standalone.utils.TestPluginProvider; +import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.PrefixNode; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -59,6 +68,7 @@ import java.time.LocalDate; import java.time.Month; import java.time.ZoneOffset; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -121,10 +131,14 @@ private static void testStorage(LuckPermsApplication app, TestPluginBootstrap bo group.setNode(DataType.NORMAL, TEST_META, true); plugin.getStorage().saveGroup(group).join(); + // try to create / save a track + Track track = plugin.getStorage().createAndLoadTrack("example", CreationCause.INTERNAL).join(); + track.setGroups(ImmutableList.of("default", "test")); + plugin.getStorage().saveTrack(track); + // try to create / save a user UUID exampleUniqueId = UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"); String exampleUsername = "Luck"; - plugin.getStorage().savePlayerData(exampleUniqueId, exampleUsername).join(); User user = plugin.getStorage().loadUser(exampleUniqueId, exampleUsername).join(); user.setNode(DataType.NORMAL, TEST_PERMISSION_1, true); @@ -134,7 +148,19 @@ private static void testStorage(LuckPermsApplication app, TestPluginBootstrap bo user.setNode(DataType.NORMAL, TEST_META, true); plugin.getStorage().saveUser(user).join(); - plugin.getStorage().loadAllGroups().join(); + // add something to the action log + LoggedAction exampleLogEntry = LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("hello 123 hello 123") + .build(); + plugin.getStorage().logAction(exampleLogEntry).join(); + + // read back the data we just saved to ensure it is as expected + plugin.getSyncTaskBuffer().requestDirectly(); Group testGroup = plugin.getGroupManager().getIfLoaded("test"); assertNotNull(testGroup); @@ -145,8 +171,27 @@ private static void testStorage(LuckPermsApplication app, TestPluginBootstrap bo assertEquals(ImmutableSet.of(Inheritance.builder("default").build(), TEST_PERMISSION_1, TEST_PERMISSION_2, TEST_GROUP, TEST_PREFIX, TEST_META), testUser.normalData().asSet()); assertTrue(exampleUsername.equalsIgnoreCase(testUser.getUsername().orElse("unknown"))); + Track testTrack = plugin.getTrackManager().getIfLoaded("example"); + assertNotNull(testTrack); + assertEquals(ImmutableList.of("default", "test"), track.getGroups()); + + Log actionLog = plugin.getStorage().getLog().join(); + assertTrue(actionLog.getContent().contains(exampleLogEntry)); + + List> groupSearchResult = plugin.getStorage().searchGroupNodes(StandardNodeMatchers.key(TEST_PERMISSION_1)).join(); + assertEquals(1, groupSearchResult.size()); + assertTrue(groupSearchResult.contains(NodeEntry.of("test", TEST_PERMISSION_1))); - // create another user + List> userSearchResult = plugin.getStorage().searchUserNodes(StandardNodeMatchers.key(TEST_PERMISSION_1)).join(); + assertEquals(1, userSearchResult.size()); + assertTrue(userSearchResult.contains(NodeEntry.of(exampleUniqueId, TEST_PERMISSION_1))); + + List> userWildcardSearchResult = plugin.getStorage().searchUserNodes(StandardNodeMatchers.type(NodeType.PREFIX)).join(); + assertEquals(1, userWildcardSearchResult.size()); + assertTrue(userWildcardSearchResult.contains(NodeEntry.of(exampleUniqueId, TEST_PREFIX))); + + + // create another user and test getUniqueUsers method UUID otherExampleUniqueId = UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"); String otherExampleUsername = "Notch"; @@ -289,6 +334,21 @@ public void testMongo(@TempDir Path tempDir) { } } + @Nested + class FlatFileDatabase { + + @Test + public void testH2(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "h2"), StorageIntegrationTest::testStorage); + } + + @Test + public void testSqlite(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "sqlite"), StorageIntegrationTest::testStorage); + } + + } + @Nested class FlatFile { @@ -299,6 +359,7 @@ public void testYaml(@TempDir Path tempDir) throws IOException { Path storageDir = tempDir.resolve("yaml-storage"); compareFiles(storageDir, "example/yaml", "groups/default.yml"); compareFiles(storageDir, "example/yaml", "groups/test.yml"); + compareFiles(storageDir, "example/yaml", "tracks/example.yml"); compareFiles(storageDir, "example/yaml", "users/c1d60c50-70b5-4722-8057-87767557e50d.yml"); } @@ -309,6 +370,7 @@ public void testJson(@TempDir Path tempDir) throws IOException { Path storageDir = tempDir.resolve("json-storage"); compareFiles(storageDir, "example/json", "groups/default.json"); compareFiles(storageDir, "example/json", "groups/test.json"); + compareFiles(storageDir, "example/json", "tracks/example.json"); compareFiles(storageDir, "example/json", "users/c1d60c50-70b5-4722-8057-87767557e50d.json"); } @@ -319,17 +381,13 @@ public void testHocon(@TempDir Path tempDir) throws IOException { Path storageDir = tempDir.resolve("hocon-storage"); compareFiles(storageDir, "example/hocon", "groups/default.conf"); compareFiles(storageDir, "example/hocon", "groups/test.conf"); + compareFiles(storageDir, "example/hocon", "tracks/example.conf"); compareFiles(storageDir, "example/hocon", "users/c1d60c50-70b5-4722-8057-87767557e50d.conf"); } @Test public void testToml(@TempDir Path tempDir) throws IOException { TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "toml"), StorageIntegrationTest::testStorage); - - Path storageDir = tempDir.resolve("toml-storage"); - compareFiles(storageDir, "example/toml", "groups/default.toml"); - compareFiles(storageDir, "example/toml", "groups/test.toml"); - compareFiles(storageDir, "example/toml", "users/c1d60c50-70b5-4722-8057-87767557e50d.toml"); } @Test @@ -365,11 +423,6 @@ public void testHoconCombined(@TempDir Path tempDir) throws IOException { @Test public void testTomlCombined(@TempDir Path tempDir) throws IOException { TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "toml-combined"), StorageIntegrationTest::testStorage); - - Path storageDir = tempDir.resolve("toml-storage"); - compareFiles(storageDir, "example/toml-combined", "groups.toml"); - compareFiles(storageDir, "example/toml-combined", "tracks.toml"); - compareFiles(storageDir, "example/toml-combined", "users.toml"); } private static void compareFiles(Path dir, String examplePath, String file) throws IOException { diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/WebEditorIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/WebEditorIntegrationTest.java new file mode 100644 index 000000000..28b43f3b2 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/WebEditorIntegrationTest.java @@ -0,0 +1,164 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.standalone; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.PermissionHolder; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.node.types.Permission; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.Predicates; +import me.lucko.luckperms.common.util.gson.GsonProvider; +import me.lucko.luckperms.common.webeditor.WebEditorRequest; +import me.lucko.luckperms.common.webeditor.WebEditorSession; +import me.lucko.luckperms.standalone.app.integration.SingletonPlayer; +import me.lucko.luckperms.standalone.utils.TestPluginProvider; +import net.luckperms.api.model.data.DataType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers(parallel = true) +@Tag("docker") +public class WebEditorIntegrationTest { + + @Container + private final GenericContainer bytebin = new GenericContainer<>(DockerImageName.parse("ghcr.io/lucko/bytebin")) + .withExposedPorts(8080); + + @Container + private final GenericContainer bytesocks = new GenericContainer<>(DockerImageName.parse("ghcr.io/lucko/bytesocks")) + .withExposedPorts(8080); + + @Test + public void testWebEditor(@TempDir Path tempDir) throws Exception { + assertTrue(this.bytebin.isRunning()); + assertTrue(this.bytesocks.isRunning()); + + String bytebinUrl = "http://" + this.bytebin.getHost() + ":" + this.bytebin.getFirstMappedPort() + "/"; + String bytesocksHost = this.bytesocks.getHost() + ":" + this.bytesocks.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("bytebin-url", bytebinUrl) + .put("bytesocks-host", bytesocksHost) + .put("bytesocks-use-tls", "false") + .build(); + + TestPluginProvider.use(tempDir, config, (app, bootstrap, plugin) -> { + + // setup some example data + UUID exampleUniqueId = UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"); + plugin.getStorage().savePlayerData(exampleUniqueId, "Luck").join(); + + User user = plugin.getStorage().loadUser(exampleUniqueId, null).join(); + user.setNode(DataType.NORMAL, Permission.builder().permission("test.node").build(), false); + plugin.getStorage().saveUser(user).join(); + + Group group = plugin.getGroupManager().getOrMake("default"); + group.setNode(DataType.NORMAL, Permission.builder().permission("other.test.node").build(), false); + plugin.getStorage().saveGroup(group).join(); + + // collect holders + List holders = new ArrayList<>(); + WebEditorRequest.includeMatchingGroups(holders, Predicates.alwaysTrue(), plugin); + WebEditorRequest.includeMatchingUsers(holders, Collections.emptyList(), true, plugin); + assertFalse(holders.isEmpty()); + + // create a new editor session + Sender sender = plugin.getSenderFactory().wrap(SingletonPlayer.INSTANCE); + WebEditorSession session = WebEditorSession.create(holders, Collections.emptyList(), sender, "lp", plugin); + String bytebinKey = session.open(); + String bytesocksKey = session.getSocket().getSocket().channelId(); + + assertNotNull(bytebinKey); + assertNotNull(bytesocksKey); + + // check bytebin payload + OkHttpClient httpClient = plugin.getHttpClient(); + + Response resp = httpClient.newCall(new Request.Builder() + .url(bytebinUrl + bytebinKey) + .build()).execute(); + assertTrue(resp.isSuccessful()); + assertEquals(200, resp.code()); + + JsonObject respObject = GsonProvider.normal().fromJson(resp.body().string(), JsonObject.class); + assertEquals(2, respObject.getAsJsonArray("permissionHolders").size()); + + // check bytesocks channel is open + CountDownLatch socketRespLatch = new CountDownLatch(1); + AtomicReference socketResp = new AtomicReference<>(); + + httpClient.newWebSocket( + new Request.Builder() + .url("ws://" + bytesocksHost + "/" + bytesocksKey) + .build(), + new WebSocketListener() { + @Override + public void onOpen(WebSocket ws, Response resp) { + socketResp.set(resp); + socketRespLatch.countDown(); + } + + @Override + public void onFailure(WebSocket ws, Throwable err, @Nullable Response resp) { + socketResp.set(resp); + socketRespLatch.countDown(); + } + }); + + assertTrue(socketRespLatch.await(5, TimeUnit.SECONDS)); + assertEquals(101, socketResp.get().code()); + }); + } +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginBootstrap.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginBootstrap.java index 6686d92a0..538ff3d9b 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginBootstrap.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginBootstrap.java @@ -126,7 +126,7 @@ public void close() { } } - static final class TestSenderFactory extends StandaloneSenderFactory { + public static final class TestSenderFactory extends StandaloneSenderFactory { private Function permissionChecker; diff --git a/standalone/src/test/resources/example/hocon-combined/tracks.conf b/standalone/src/test/resources/example/hocon-combined/tracks.conf index 8b1378917..f8018594f 100644 --- a/standalone/src/test/resources/example/hocon-combined/tracks.conf +++ b/standalone/src/test/resources/example/hocon-combined/tracks.conf @@ -1 +1,6 @@ - +example { + groups=[ + default, + test + ] +} diff --git a/standalone/src/test/resources/example/hocon/tracks/example.conf b/standalone/src/test/resources/example/hocon/tracks/example.conf new file mode 100644 index 000000000..604648fa8 --- /dev/null +++ b/standalone/src/test/resources/example/hocon/tracks/example.conf @@ -0,0 +1,5 @@ +groups=[ + default, + test +] +name=example diff --git a/standalone/src/test/resources/example/json-combined/tracks.json b/standalone/src/test/resources/example/json-combined/tracks.json index 0967ef424..73b87b6fe 100644 --- a/standalone/src/test/resources/example/json-combined/tracks.json +++ b/standalone/src/test/resources/example/json-combined/tracks.json @@ -1 +1,8 @@ -{} +{ + "example": { + "groups": [ + "default", + "test" + ] + } +} diff --git a/standalone/src/test/resources/example/json/tracks/example.json b/standalone/src/test/resources/example/json/tracks/example.json new file mode 100644 index 000000000..63268fbc4 --- /dev/null +++ b/standalone/src/test/resources/example/json/tracks/example.json @@ -0,0 +1,7 @@ +{ + "name": "example", + "groups": [ + "default", + "test" + ] +} diff --git a/standalone/src/test/resources/example/toml-combined/groups.toml b/standalone/src/test/resources/example/toml-combined/groups.toml deleted file mode 100644 index 5fdf4f04d..000000000 --- a/standalone/src/test/resources/example/toml-combined/groups.toml +++ /dev/null @@ -1,33 +0,0 @@ - [[test.prefixes]] - prefix = "TEST" - priority = 100 - - [test.prefixes.context] - server = "foo" - world = "bar" - [[test.permissions]] - permission = "group.default" - expiry = 2532384000 - value = false - - [test.permissions.context] - server = "foo" - world = "bar" - test = "test" - - [[test.permissions]] - permission = "test" - expiry = 2532384000 - value = false - - [test.permissions.context] - server = "foo" - world = "bar" - test = "test" - - [[test.permissions]] - permission = "example.permission" - value = true - [[test.meta]] - value = "bar" - key = "foo" diff --git a/standalone/src/test/resources/example/toml-combined/tracks.toml b/standalone/src/test/resources/example/toml-combined/tracks.toml deleted file mode 100644 index e69de29bb..000000000 diff --git a/standalone/src/test/resources/example/toml-combined/users.toml b/standalone/src/test/resources/example/toml-combined/users.toml deleted file mode 100644 index 28453a132..000000000 --- a/standalone/src/test/resources/example/toml-combined/users.toml +++ /dev/null @@ -1,42 +0,0 @@ -[c1d60c50-70b5-4722-8057-87767557e50d] - name = "Luck" - primary-group = "default" - - [[c1d60c50-70b5-4722-8057-87767557e50d.permissions]] - permission = "group.default" - value = false - expiry = 2532384000 - - [c1d60c50-70b5-4722-8057-87767557e50d.permissions.context] - server = "foo" - test = "test" - world = "bar" - - [[c1d60c50-70b5-4722-8057-87767557e50d.permissions]] - permission = "test" - value = false - expiry = 2532384000 - - [c1d60c50-70b5-4722-8057-87767557e50d.permissions.context] - server = "foo" - test = "test" - world = "bar" - - [[c1d60c50-70b5-4722-8057-87767557e50d.permissions]] - permission = "example.permission" - value = true - - [[c1d60c50-70b5-4722-8057-87767557e50d.parents]] - group = "default" - - [[c1d60c50-70b5-4722-8057-87767557e50d.prefixes]] - prefix = "TEST" - priority = 100 - - [c1d60c50-70b5-4722-8057-87767557e50d.prefixes.context] - server = "foo" - world = "bar" - - [[c1d60c50-70b5-4722-8057-87767557e50d.meta]] - key = "foo" - value = "bar" diff --git a/standalone/src/test/resources/example/toml/groups/default.toml b/standalone/src/test/resources/example/toml/groups/default.toml deleted file mode 100644 index 0df4b23ea..000000000 --- a/standalone/src/test/resources/example/toml/groups/default.toml +++ /dev/null @@ -1 +0,0 @@ -name = "default" diff --git a/standalone/src/test/resources/example/toml/groups/test.toml b/standalone/src/test/resources/example/toml/groups/test.toml deleted file mode 100644 index 206e86ee0..000000000 --- a/standalone/src/test/resources/example/toml/groups/test.toml +++ /dev/null @@ -1,37 +0,0 @@ -name = "test" - -[[permissions]] - permission = "group.default" - value = false - expiry = 2532384000 - - [permissions.context] - server = "foo" - test = "test" - world = "bar" - -[[permissions]] - permission = "test" - value = false - expiry = 2532384000 - - [permissions.context] - server = "foo" - test = "test" - world = "bar" - -[[permissions]] - permission = "example.permission" - value = true - -[[prefixes]] - prefix = "TEST" - priority = 100 - - [prefixes.context] - server = "foo" - world = "bar" - -[[meta]] - key = "foo" - value = "bar" diff --git a/standalone/src/test/resources/example/toml/users/c1d60c50-70b5-4722-8057-87767557e50d.toml b/standalone/src/test/resources/example/toml/users/c1d60c50-70b5-4722-8057-87767557e50d.toml deleted file mode 100644 index 12371883e..000000000 --- a/standalone/src/test/resources/example/toml/users/c1d60c50-70b5-4722-8057-87767557e50d.toml +++ /dev/null @@ -1,42 +0,0 @@ -uuid = "c1d60c50-70b5-4722-8057-87767557e50d" -name = "Luck" -primary-group = "default" - -[[permissions]] - permission = "group.default" - value = false - expiry = 2532384000 - - [permissions.context] - server = "foo" - test = "test" - world = "bar" - -[[permissions]] - permission = "test" - value = false - expiry = 2532384000 - - [permissions.context] - server = "foo" - test = "test" - world = "bar" - -[[permissions]] - permission = "example.permission" - value = true - -[[parents]] - group = "default" - -[[prefixes]] - prefix = "TEST" - priority = 100 - - [prefixes.context] - server = "foo" - world = "bar" - -[[meta]] - key = "foo" - value = "bar" diff --git a/standalone/src/test/resources/example/yaml-combined/tracks.yml b/standalone/src/test/resources/example/yaml-combined/tracks.yml index 19765bd50..5482d7ad9 100644 --- a/standalone/src/test/resources/example/yaml-combined/tracks.yml +++ b/standalone/src/test/resources/example/yaml-combined/tracks.yml @@ -1 +1,4 @@ -null +example: + groups: + - default + - test diff --git a/standalone/src/test/resources/example/yaml/tracks/example.yml b/standalone/src/test/resources/example/yaml/tracks/example.yml new file mode 100644 index 000000000..44843212f --- /dev/null +++ b/standalone/src/test/resources/example/yaml/tracks/example.yml @@ -0,0 +1,4 @@ +name: example +groups: +- default +- test From 8fbd79139d34c97e13a486db554d12415f2a1fc2 Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 25 Jun 2023 22:36:31 +0100 Subject: [PATCH 19/23] Add missing future join call to storage test --- .../me/lucko/luckperms/standalone/StorageIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java index 0592c2532..d7f1af829 100644 --- a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java @@ -134,7 +134,7 @@ private static void testStorage(LuckPermsApplication app, TestPluginBootstrap bo // try to create / save a track Track track = plugin.getStorage().createAndLoadTrack("example", CreationCause.INTERNAL).join(); track.setGroups(ImmutableList.of("default", "test")); - plugin.getStorage().saveTrack(track); + plugin.getStorage().saveTrack(track).join(); // try to create / save a user UUID exampleUniqueId = UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"); From 6d8b6bd3feadd7e7a414c8d776cd2cbe74a64eb9 Mon Sep 17 00:00:00 2001 From: Luck Date: Sat, 5 Aug 2023 09:49:56 +0100 Subject: [PATCH 20/23] Add jacoco report gradle plugin --- common/build.gradle | 5 +++++ standalone/build.gradle | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/common/build.gradle b/common/build.gradle index a8b533e02..c2b3f51f7 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,11 +1,16 @@ plugins { id("java-library") + id("jacoco") } test { useJUnitPlatform {} } +jacocoTestReport { + dependsOn test +} + dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.1' diff --git a/standalone/build.gradle b/standalone/build.gradle index aa4023596..7bd759f5d 100644 --- a/standalone/build.gradle +++ b/standalone/build.gradle @@ -1,5 +1,7 @@ plugins { alias(libs.plugins.shadow) + id("jacoco") + id("jacoco-report-aggregation") } sourceCompatibility = 17 @@ -13,6 +15,10 @@ test { } } +jacocoTestReport { + dependsOn test +} + dependencies { implementation project(':common') compileOnly project(':common:loader-utils') From b8d1f52d7de9cb5a28404468b5facfb9ae7b6968 Mon Sep 17 00:00:00 2001 From: Luck Date: Sat, 5 Aug 2023 10:09:30 +0100 Subject: [PATCH 21/23] Use ansi component serializer --- README.md | 2 +- standalone/app/build.gradle | 17 ++- .../app/integration/SingletonPlayer.java | 5 +- .../standalone/app/utils/AnsiUtils.java | 130 ------------------ standalone/build.gradle | 1 + .../standalone/LPStandaloneBootstrap.java | 5 +- 6 files changed, 19 insertions(+), 141 deletions(-) delete mode 100644 standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/AnsiUtils.java diff --git a/README.md b/README.md index 8782a9c97..83e8f0c82 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ For more information, see the wiki article on [Why LuckPerms?](https://luckperms LuckPerms uses Gradle to handle dependencies & building. #### Requirements -* Java 8 JDK or newer +* Java 17 JDK or newer * Git #### Compiling from source diff --git a/standalone/app/build.gradle b/standalone/app/build.gradle index ee7edfdb9..708f45f97 100644 --- a/standalone/app/build.gradle +++ b/standalone/app/build.gradle @@ -18,21 +18,21 @@ dependencies { api 'com.google.guava:guava:31.1-jre' api 'io.netty:netty-all:4.1.93.Final' - api('net.kyori:adventure-api:4.11.0') { + api('net.kyori:adventure-api:4.14.0') { exclude(module: 'adventure-bom') exclude(module: 'checker-qual') exclude(module: 'annotations') } - api('net.kyori:adventure-text-serializer-gson:4.11.0') { + api('net.kyori:adventure-text-serializer-gson:4.14.0') { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') exclude(module: 'gson') } - api('net.kyori:adventure-text-serializer-legacy:4.11.0') { + api('net.kyori:adventure-text-serializer-legacy:4.14.0') { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') } - api('net.kyori:adventure-text-serializer-plain:4.11.0') { + api('net.kyori:adventure-text-serializer-plain:4.14.0') { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') } @@ -40,7 +40,14 @@ dependencies { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') } - api('net.kyori:ansi:1.0.1') + api('net.kyori:adventure-text-serializer-ansi:4.14.0') { + exclude(module: 'adventure-bom') + exclude(module: 'adventure-api') + exclude(module: 'annotations') + } + api('net.kyori:ansi:1.0.3') { + exclude(module: 'annotations') + } } blossom { diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java index 8d2453f74..0df81372b 100644 --- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/SingletonPlayer.java @@ -26,8 +26,9 @@ package me.lucko.luckperms.standalone.app.integration; import me.lucko.luckperms.standalone.app.LuckPermsApplication; -import me.lucko.luckperms.standalone.app.utils.AnsiUtils; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer; +import net.kyori.ansi.ColorLevel; import java.util.Set; import java.util.UUID; @@ -46,7 +47,7 @@ public class SingletonPlayer { private static final UUID UUID = new UUID(0, 0); /** A message sink that prints the component to stdout */ - private static final Consumer PRINT_TO_STDOUT = component -> LuckPermsApplication.LOGGER.info(AnsiUtils.format(component)); + private static final Consumer PRINT_TO_STDOUT = component -> LuckPermsApplication.LOGGER.info(ANSIComponentSerializer.ansi().serialize(component)); /** Singleton instance */ public static final SingletonPlayer INSTANCE = new SingletonPlayer(); diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/AnsiUtils.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/AnsiUtils.java deleted file mode 100644 index 048831e32..000000000 --- a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/AnsiUtils.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * This file is part of LuckPerms, licensed under the MIT License. - * - * Copyright (c) lucko (Luck) - * Copyright (c) contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package me.lucko.luckperms.standalone.app.utils; - -import net.kyori.adventure.key.Key; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.flattener.ComponentFlattener; -import net.kyori.adventure.text.flattener.FlattenerListener; -import net.kyori.adventure.text.format.Style; -import net.kyori.adventure.text.format.TextColor; -import net.kyori.adventure.text.format.TextDecoration; -import net.kyori.ansi.ANSIComponentRenderer; -import net.kyori.ansi.StyleOps; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.Range; - -/** - * Utility to format a {@link Component} as an ANSI string. - */ -public final class AnsiUtils { - private AnsiUtils() {} - - public static String format(Component component) { - ANSIComponentRenderer.ToString