diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..022b84144 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 4754ae307..000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: 🐛 ​ Bug report -about: Report an issue or problem with the way LuckPerms is functioning. -title: '' -labels: 'type: issue' -assignees: '' - ---- - - - -### Description - - - -### Reproduction steps - - -1. Open the config.yml file and set example to true. -2. Restart the server -3. Run /lp user example permission set x from the console -4. See error - - -### Expected behaviour - - - -### Environment details - - -* Server type/version: `ExampleSpigot` running version `1.12.2` build `???` -* LuckPerms version: `v???` - - - - -### Any other relevant details - - diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..fc6881422 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,63 @@ +name: 🐛 ​ Bug report +description: Report an issue or problem with the way LuckPerms is functioning. +labels: ["type: issue"] +body: + - type: markdown + attributes: + value: "Before going any further, please check to see if your problem is fixed by updating to a newer version. The latest development builds can be obtained from https://luckperms.net/download" + - type: textarea + id: description + attributes: + label: Description + description: Please provide a short description of the issue in the space below. + placeholder: Description goes here! + validations: + required: true + - type: textarea + id: reproduction-steps + attributes: + label: Reproduction Steps + description: Please provide a clear list of steps we should follow in order to reproduce your issue in the space below. + placeholder: List reproduction steps here, please! + validations: + required: true + - type: textarea + id: expected-behaviour + attributes: + label: Expected Behaviour + description: Please provide a description of what you expected to happen. + placeholder: Describe what you expected to happen here. + validations: + required: true + - type: input + id: server-info + attributes: + label: Server Details + description: Please provide a description of the server details, including type and specific version number. + placeholder: 'git-Paper-124 (MC: 1.17.1)' + validations: + required: true + - type: input + id: luckperms-version + attributes: + label: LuckPerms Version + description: Please provide the specific, precise version number of LuckPerms you are using to reproduce this bug. + placeholder: v5.3.70 + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs and Configs + description: If you think it would be helpful, please also include a pastebin of any relevant config files or log output. You should use https://gist.github.com/ to upload, then include the link below. + placeholder: https://gist.github.com/HelpfulBugReporter/ThisIsAVeryUsefulLogOutput + validations: + required: false + - type: textarea + id: extra-info + attributes: + label: Extra Details + description: Please include any other relevant details in the space below. + placeholder: I was able to reproduce this only at the end of a rainbow. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/suggestion.md b/.github/ISSUE_TEMPLATE/suggestion.md deleted file mode 100644 index cae60ae12..000000000 --- a/.github/ISSUE_TEMPLATE/suggestion.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: 💡 ​ Suggestion -about: Suggest an idea for an improvement or change to LuckPerms. -title: '' -labels: 'type: suggestion' -assignees: '' - ---- - -### Description - - - -### Proposed behaviour - - diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml new file mode 100644 index 000000000..12e8852ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggestion.yml @@ -0,0 +1,31 @@ +name: 💡 ​ Suggestion +description: Suggest an idea for an improvement or change to LuckPerms. +labels: ["type: suggestion"] +body: + - type: markdown + attributes: + value: "Before going any further, please check to see if your suggestion has already been added by checking the [wiki](https://luckperms.net/wiki/Home). The latest development builds can be obtained from https://luckperms.net/download" + - type: textarea + id: description + attributes: + label: Description + description: Please provide a short description of your suggestion in the space below. + placeholder: Hello, perhaps you should... + validations: + required: true + - type: textarea + id: proposed-behaviour + attributes: + label: Proposed Behaviour + description: Please provide a short explanation of how the feature should work / be changed, and how this will affect the project. + placeholder: It would... + validations: + required: true + - type: textarea + id: extra-info + attributes: + label: Extra Details + description: Please include any other relevant details in the space below. + placeholder: I think this should only function at the end of a rainbow. + validations: + required: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..f62949e89 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,128 @@ +name: Build Gradle and Publish Docker image + +on: + push: + branches: + - 'master' + tags: + - 'v*' + pull_request: + branches: + - 'master' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-gradle: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '25' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Run build and tests with Gradle wrapper + run: ./gradlew test build -PdockerTests + + - name: Publish test report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' + annotate_notice: true + detailed_summary: true + + - name: Upload all artifacts + uses: actions/upload-artifact@v4 + with: + name: jars + path: | + bukkit/loader/build/libs/LuckPerms-Bukkit-*.jar + bukkit-legacy/loader/build/libs/LuckPerms-Bukkit-Legacy-*.jar + bungee/loader/build/libs/LuckPerms-Bungee-*.jar + sponge/loader/build/libs/LuckPerms-Sponge-*.jar + nukkit/loader/build/libs/LuckPerms-Nukkit-*.jar + velocity/build/libs/LuckPerms-Velocity-*.jar + fabric/build/libs/LuckPerms-Fabric-*.jar + forge/loader/build/libs/LuckPerms-Forge-*.jar + standalone/loader/build/libs/LuckPerms-Standalone-*.jar + + - name: Upload standalone artifact + uses: actions/upload-artifact@v4 + with: + name: standalone-binary + path: standalone/loader/build/libs/LuckPerms-Standalone-*.jar + + + build-docker: + needs: build-gradle + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Retrieve saved standalone jar artifact + uses: actions/download-artifact@v4 + with: + name: standalone-binary + path: standalone/docker/ + + - name: Remove version number from jar artifact name + run: mv standalone/docker/LuckPerms-Standalone-*.jar standalone/docker/luckperms-standalone.jar + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + 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@v5 + with: + 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@v6 + with: + context: standalone/docker/ + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} 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/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30037813a..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 @@ -23,4 +23,4 @@ The project is split up into a few separate modules. * **API** - The public, semantically versioned API used by other plugins wishing to integrate with and retrieve data from LuckPerms. This module (for the most part) does not contain any implementation itself, and is provided by the plugin. * **Common** - The common module contains most of the code which implements the respective LuckPerms plugins. This abstract module reduces duplicated code throughout the project. -* **Bukkit, BungeeCord, Sponge, Nukkit, Velocity & Fabric** - Each use the common module to implement plugins on the respective server platforms. +* **Bukkit, BungeeCord, Fabric, Forge, Nukkit, Sponge & Velocity** - Each use the common module to implement plugins on the respective server platforms. diff --git a/README.md b/README.md index 56ac8b795..35476e412 100644 --- a/README.md +++ b/README.md @@ -24,18 +24,24 @@ 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 21 JDK or newer * Git #### Compiling from source ```sh -git clone https://github.com/lucko/LuckPerms.git +git clone https://github.com/LuckPerms/LuckPerms.git cd LuckPerms/ ./gradlew build ``` You can find the output jars in the `loader/build/libs` or `build/libs` directories. +## Tests +There are some automated tests which run during each build. + +* Unit tests are defined in [`common/src/test`](https://github.com/LuckPerms/LuckPerms/tree/master/common/src/test) +* Integration tests are defined in [`standalone/src/test`](https://github.com/LuckPerms/LuckPerms/tree/master/standalone/src/test). + ## Contributing #### Pull Requests If you make any changes or improvements to the plugin which you think would be beneficial to others, please consider making a pull request to merge your changes back into the upstream project. (especially if your changes are bug fixes!) @@ -47,7 +53,7 @@ The project is split up into a few separate modules. * **API** - The public, semantically versioned API used by other plugins wishing to integrate with and retrieve data from LuckPerms. This module (for the most part) does not contain any implementation itself, and is provided by the plugin. * **Common** - The common module contains most of the code which implements the respective LuckPerms plugins. This abstract module reduces duplicated code throughout the project. -* **Bukkit, BungeeCord, Sponge, Fabric, Nukkit & Velocity** - Each use the common module to implement plugins on the respective server platforms. +* **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/api/build.gradle b/api/build.gradle index 02d3dd16d..1aa56c3a0 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,9 +1,19 @@ group = 'net.luckperms' -project.version = '5.3' +project.version = '5.5' + +tasks.withType(JavaCompile).configureEach { + options.release = 8 +} + +jar { + manifest { + attributes('Automatic-Module-Name': 'net.luckperms.api') + } +} dependencies { - compileOnly 'org.checkerframework:checker-qual:3.12.0' - compileOnly 'org.jetbrains:annotations:20.1.0' + compileOnly 'org.checkerframework:checker-qual:3.49.3' + compileOnly 'org.jetbrains:annotations:26.0.2' } // Only used occasionally for deployment - not needed for normal builds. @@ -18,92 +28,83 @@ if (project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePass options.charSet = 'UTF-8' options.links( 'https://checkerframework.org/api/', - 'https://javadoc.io/static/org.jetbrains/annotations/20.1.0/' + 'https://javadoc.io/static/org.jetbrains/annotations/26.0.2/' ) options.addStringOption('Xdoclint:none', '-quiet') + options.addStringOption('-since', '5.0,5.1,5.2,5.3,5.4,5.5') if (JavaVersion.current() > JavaVersion.VERSION_1_8) { - options.addBooleanOption('-no-module-directories', true) - options.links.add('https://docs.oracle.com/en/java/javase/11/docs/api/') + options.links.add('https://docs.oracle.com/en/java/javase/21/docs/api/') } else { options.links.add('https://docs.oracle.com/javase/8/docs/api/') } } - task javadocJar(type: Jar, dependsOn: javadoc) { - classifier 'javadoc' - from javadoc.destinationDir - } - - task sourcesJar(type: Jar) { - classifier 'sources' - from sourceSets.main.allSource + java { + withJavadocJar() + withSourcesJar() } - artifacts { - archives javadocJar - archives sourcesJar - } - - signing { - useGpgCmd() - sign configurations.archives - } - - uploadArchives { + publishing { repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') { - authentication(userName: sonatypeUsername, password: sonatypePassword) + maven { + def releasesRepoUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' + def snapshotsRepoUrl = 'https://oss.sonatype.org/content/repositories/snapshots/' + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + + credentials { + username sonatypeUsername + password sonatypePassword } + } + } - snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } + publications { + mavenJava(MavenPublication) { + artifactId = 'api' - pom { - project { - name 'LuckPerms API' - description 'A permissions plugin for Minecraft servers.' - url 'https://luckperms.net' - - licenses { - license { - name 'MIT' - url 'https://opensource.org/licenses/MIT' - } - } + from components.java - developers { - developer { - id 'lucko' - name 'Luck' - url 'https://lucko.me' - email 'git@lucko.me' - } + pom { + name = 'LuckPerms API' + description = 'A permissions manager for Minecraft servers.' + url = 'https://luckperms.net' + + licenses { + license { + name = 'MIT' + url = 'https://opensource.org/licenses/MIT' } + } - scm { - connection 'scm:git:https://github.com/lucko/LuckPerms.git' - developerConnection 'scm:git:git@github.com:lucko/LuckPerms.git' - url 'https://github.com/lucko/LuckPerms' + developers { + developer { + id = 'lucko' + name = 'Luck' + url = 'https://lucko.me' + email = 'git@lucko.me' } + } - ciManagement { - system 'Jenkins' - url 'https://ci.lucko.me/job/LuckPerms' - } + scm { + connection = 'scm:git:https://github.com/LuckPerms/LuckPerms.git' + developerConnection = 'scm:git:git@github.com:LuckPerms/LuckPerms.git' + url = 'https://github.com/LuckPerms/LuckPerms' + } - issueManagement { - system 'GitHub' - url 'https://github.com/lucko/LuckPerms/issues' - } + issueManagement { + system = 'GitHub' + url = 'https://github.com/LuckPerms/LuckPerms/issues' } } } } } + + signing { + useGpgCmd() + sign publishing.publications.mavenJava + required = true + } } */ diff --git a/api/javadoc/overview.html b/api/javadoc/overview.html index 061a300b6..e76b31e9c 100644 --- a/api/javadoc/overview.html +++ b/api/javadoc/overview.html @@ -1,6 +1,6 @@

- LuckPerms is a permissions plugin for Minecraft servers. It allows server admins to control what + LuckPerms is a permissions manager for Minecraft servers. It allows server admins to control what features players can use by creating groups and assigning permissions.

Useful Links

@@ -8,10 +8,11 @@

Useful Links

  • Project Website
  • Wiki - API Introduction
  • Wiki - API Usage
  • +
  • Source Code
  • Maven

    - The API artifact is deployed to Maven Central under + The API artifact is deployed to Maven Central with group id: net.luckperms, artifact id: api. diff --git a/api/src/main/java/net/luckperms/api/LuckPerms.java b/api/src/main/java/net/luckperms/api/LuckPerms.java index ddd07606f..6c851c45c 100644 --- a/api/src/main/java/net/luckperms/api/LuckPerms.java +++ b/api/src/main/java/net/luckperms/api/LuckPerms.java @@ -26,6 +26,7 @@ package net.luckperms.api; import net.luckperms.api.actionlog.ActionLogger; +import net.luckperms.api.actionlog.filter.ActionFilterFactory; import net.luckperms.api.context.ContextCalculator; import net.luckperms.api.context.ContextManager; import net.luckperms.api.event.EventBus; @@ -40,13 +41,13 @@ import net.luckperms.api.model.user.UserManager; import net.luckperms.api.node.NodeBuilderRegistry; import net.luckperms.api.node.matcher.NodeMatcherFactory; +import net.luckperms.api.platform.Health; import net.luckperms.api.platform.Platform; import net.luckperms.api.platform.PlayerAdapter; import net.luckperms.api.platform.PluginMetadata; import net.luckperms.api.query.QueryOptionsRegistry; import net.luckperms.api.track.Track; import net.luckperms.api.track.TrackManager; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.Internal; @@ -220,6 +221,17 @@ public interface LuckPerms { */ @NonNull CompletableFuture runUpdateTask(); + /** + * Executes a health check. + * + *

    This task checks if the LuckPerms implementation is running and + * whether it has a connection to the database (if applicable).

    + * + * @return the health status + * @since 5.5 + */ + @NonNull Health runHealthCheck(); + /** * Registers a {@link MessengerProvider} for use by the platform. * @@ -256,4 +268,14 @@ public interface LuckPerms { @Internal @NonNull NodeMatcherFactory getNodeMatcherFactory(); + /** + * Gets the {@link ActionFilterFactory}. + * + * @return the action filter factory + * @since 5.5 + */ + @Internal + @NonNull + ActionFilterFactory getActionFilterFactory(); + } diff --git a/api/src/main/java/net/luckperms/api/LuckPermsProvider.java b/api/src/main/java/net/luckperms/api/LuckPermsProvider.java index 4f9579aa8..4f36c2d28 100644 --- a/api/src/main/java/net/luckperms/api/LuckPermsProvider.java +++ b/api/src/main/java/net/luckperms/api/LuckPermsProvider.java @@ -79,7 +79,8 @@ private static final class NotLoadedException extends IllegalStateException { " a) the LuckPerms plugin is not installed or it failed to enable\n" + " b) the plugin in the stacktrace does not declare a dependency on LuckPerms\n" + " c) the plugin in the stacktrace is retrieving the API before the plugin 'enable' phase\n" + - " (call the #get method in onEnable, not the constructor!)\n"; + " (call the #get method in onEnable, not the constructor!)\n" + + " d) the plugin in the stacktrace is incorrectly 'shading' the LuckPerms API into its jar\n"; NotLoadedException() { super(MESSAGE); diff --git a/api/src/main/java/net/luckperms/api/actionlog/Action.java b/api/src/main/java/net/luckperms/api/actionlog/Action.java index 8b000996c..53260674c 100644 --- a/api/src/main/java/net/luckperms/api/actionlog/Action.java +++ b/api/src/main/java/net/luckperms/api/actionlog/Action.java @@ -26,9 +26,9 @@ package net.luckperms.api.actionlog; import net.luckperms.api.LuckPermsProvider; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.ApiStatus.NonExtendable; import java.time.Instant; import java.util.Optional; @@ -36,7 +36,10 @@ /** * Represents a logged action. + * + *

    API users should not implement this interface directly.

    */ +@NonExtendable public interface Action extends Comparable { /** @@ -82,6 +85,7 @@ public interface Action extends Comparable { /** * Represents the source of an action. */ + @NonExtendable interface Source { /** @@ -103,6 +107,7 @@ interface Source { /** * Represents the target of an action. */ + @NonExtendable interface Target { /** @@ -127,7 +132,7 @@ interface Target { @NonNull Type getType(); /** - * Represents the type of a {@link Target}. + * Represents the type of {@link Target}. */ enum Type { USER, GROUP, TRACK diff --git a/api/src/main/java/net/luckperms/api/actionlog/ActionLog.java b/api/src/main/java/net/luckperms/api/actionlog/ActionLog.java index bba6eb457..131ab3a5c 100644 --- a/api/src/main/java/net/luckperms/api/actionlog/ActionLog.java +++ b/api/src/main/java/net/luckperms/api/actionlog/ActionLog.java @@ -25,6 +25,7 @@ package net.luckperms.api.actionlog; +import net.luckperms.api.actionlog.filter.ActionFilter; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; @@ -40,7 +41,11 @@ * You can add to the log using the {@link ActionLogger}, and then request an updated copy.

    * *

    All methods are thread safe, and return immutable and thread safe collections.

    + * + * @deprecated Use {@link ActionLogger#queryActions(ActionFilter)} or + * {@link ActionLogger#queryActions(ActionFilter, int, int)} instead. */ +@Deprecated public interface ActionLog { /** diff --git a/api/src/main/java/net/luckperms/api/actionlog/ActionLogger.java b/api/src/main/java/net/luckperms/api/actionlog/ActionLogger.java index 16e12ffde..8b8eb6dca 100644 --- a/api/src/main/java/net/luckperms/api/actionlog/ActionLogger.java +++ b/api/src/main/java/net/luckperms/api/actionlog/ActionLogger.java @@ -25,10 +25,11 @@ package net.luckperms.api.actionlog; -import net.luckperms.api.messaging.MessagingService; - +import net.luckperms.api.actionlog.filter.ActionFilter; +import net.luckperms.api.util.Page; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.List; import java.util.concurrent.CompletableFuture; /** @@ -47,43 +48,78 @@ public interface ActionLogger { * Gets a {@link ActionLog} instance from the plugin storage. * * @return a log instance + * @deprecated Use {@link #queryActions(ActionFilter)} or {@link #queryActions(ActionFilter, int, int)} instead. These methods + * are more efficient (they don't load the full action log into memory) and allow for pagination. */ + @Deprecated @NonNull CompletableFuture getLog(); /** - * Submits a log entry to the plugin to be handled. + * Gets all actions from the action log matching the given {@code filter}. + * + *

    If the filter is {@code null}, all actions will be returned.

    + * + *

    Unlike {@link #queryActions(ActionFilter, int, int)}, this method does not implement any pagination and will return + * all entries at once.

    + * + * @param filter the filter, optional + * @return the actions + * @since 5.5 + */ + @NonNull CompletableFuture> queryActions(@NonNull ActionFilter filter); + + /** + * Gets a page of actions from the action log matching the given {@code filter}. * - *

    This method submits the log to the storage provider and broadcasts - * it.

    + *

    If the filter is {@code null}, all actions will be returned.

    * - *

    It is therefore roughly equivalent to calling - * {@link #submitToStorage(Action)} and {@link #broadcastAction(Action)}, - * however, using this method is preferred to making the calls individually.

    + * @param filter the filter, optional + * @param pageSize the size of the page + * @param pageNumber the page number + * @return the page of actions + * @since 5.5 + */ + @NonNull CompletableFuture> queryActions(@NonNull ActionFilter filter, int pageSize, int pageNumber); + + /** + * Submits a logged action to LuckPerms. + * + *

    This method submits the action to the storage provider to be persisted in the action log. + * It also broadcasts it to administrator players on the current instance and to admins on other + * connected servers if a messaging service is configured.

    * - *

    If you want to submit a log entry but don't know which method to pick, + *

    It is roughly equivalent to calling + * {@link #submitToStorage(Action)} followed by {@link #broadcastAction(Action)}, + * however using this method is preferred to making the calls individually.

    + * + *

    If you want to submit an action log entry but don't know which method to pick, * use this one.

    * * @param entry the entry to submit - * @return a future which will complete when the action is done + * @return a future which will complete when the action is submitted */ @NonNull CompletableFuture submit(@NonNull Action entry); /** - * Submits a log entry to the plugins storage handler. + * Submits a logged action to LuckPerms and persists it in the storage backend. + * + *

    This method does not broadcast the action or send it through the messaging service.

    * * @param entry the entry to submit - * @return a future which will complete when the action is done + * @return a future which will complete when the action is submitted */ @NonNull CompletableFuture submitToStorage(@NonNull Action entry); /** - * Submits a log entry to the plugins log broadcasting handler. + * Submits a logged action to LuckPerms and broadcasts it to administrators. + * + *

    The broadcast is made to administrator players on the current instance + * and to admins on other connected servers if a messaging service is configured.

    * - *

    If enabled, this method will also dispatch the log entry via the - * plugins {@link MessagingService}.

    + *

    This method does not save the action to the plugin storage backend.

    * * @param entry the entry to submit - * @return a future which will complete when the action is done + * @return a future which will complete when the action is broadcasted */ @NonNull CompletableFuture broadcastAction(@NonNull Action entry); diff --git a/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilter.java b/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilter.java new file mode 100644 index 000000000..6614cbb34 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilter.java @@ -0,0 +1,114 @@ +/* + * 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 net.luckperms.api.actionlog.filter; + +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.actionlog.Action; +import org.jetbrains.annotations.ApiStatus.NonExtendable; + +import java.util.UUID; +import java.util.function.Predicate; + +/** + * A predicate filter which matches certain {@link Action}s. + * + *

    API users should not implement this interface directly.

    + * + * @since 5.5 + */ +@NonExtendable +public interface ActionFilter extends Predicate { + + /** + * Gets an {@link ActionFilter} which matches any action. + * + * @return the matcher + */ + static ActionFilter any() { + return LuckPermsProvider.get().getActionFilterFactory().any(); + } + + /** + * Gets an {@link ActionFilter} which matches actions with a specific source user. + * + * @param uniqueId the source user unique id + * @return the matcher + */ + static ActionFilter source(UUID uniqueId) { + return LuckPermsProvider.get().getActionFilterFactory().source(uniqueId); + } + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific user. + * + * @param uniqueId the target user unique id + * @return the matcher + */ + static ActionFilter user(UUID uniqueId) { + return LuckPermsProvider.get().getActionFilterFactory().user(uniqueId); + } + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific group. + * + * @param name the target group name + * @return the matcher + */ + static ActionFilter group(String name) { + return LuckPermsProvider.get().getActionFilterFactory().group(name); + } + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific track. + * + * @param name the target track name + * @return the matcher + */ + static ActionFilter track(String name) { + return LuckPermsProvider.get().getActionFilterFactory().track(name); + } + + /** + * Gets an {@link ActionFilter} which matches actions which contain a specific search query in the source name, + * target name or description. + * + * @param query the search query + * @return the matcher + */ + static ActionFilter search(String query) { + return LuckPermsProvider.get().getActionFilterFactory().search(query); + } + + /** + * Tests to see if the given {@link Action} matches the filter. + * + * @param action the action to test + * @return true if the action matched + */ + @Override + boolean test(Action action); + +} diff --git a/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilterFactory.java b/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilterFactory.java new file mode 100644 index 000000000..31e699ac5 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/actionlog/filter/ActionFilterFactory.java @@ -0,0 +1,88 @@ +/* + * 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 net.luckperms.api.actionlog.filter; + +import org.jetbrains.annotations.ApiStatus.Internal; + +import java.util.UUID; + +/** + * A factory which creates {@link ActionFilter}s. + * + * @since 5.5 + */ +@Internal +public interface ActionFilterFactory { + + /** + * Gets an {@link ActionFilter} which matches any action. + * + * @return the matcher + */ + ActionFilter any(); + + /** + * Gets an {@link ActionFilter} which matches actions with a specific source user. + * + * @param uniqueId the source user unique id + * @return the matcher + */ + ActionFilter source(UUID uniqueId); + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific user. + * + * @param uniqueId the target user unique id + * @return the matcher + */ + ActionFilter user(UUID uniqueId); + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific group. + * + * @param name the target group name + * @return the matcher + */ + ActionFilter group(String name); + + /** + * Gets an {@link ActionFilter} which matches actions which target a specific track. + * + * @param name the target track name + * @return the matcher + */ + ActionFilter track(String name); + + /** + * Gets an {@link ActionFilter} which matches actions which contain a specific search query in the source name, + * target name or description. + * + * @param query the search query + * @return the matcher + */ + ActionFilter search(String query); + +} diff --git a/api/src/main/java/net/luckperms/api/actionlog/filter/package-info.java b/api/src/main/java/net/luckperms/api/actionlog/filter/package-info.java new file mode 100644 index 000000000..2eebf3be8 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/actionlog/filter/package-info.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** + * {@link net.luckperms.api.actionlog.Action} filters. + */ +package net.luckperms.api.actionlog.filter; \ No newline at end of file diff --git a/api/src/main/java/net/luckperms/api/cacheddata/CachedData.java b/api/src/main/java/net/luckperms/api/cacheddata/CachedData.java index 709ebff30..e2e1eec77 100644 --- a/api/src/main/java/net/luckperms/api/cacheddata/CachedData.java +++ b/api/src/main/java/net/luckperms/api/cacheddata/CachedData.java @@ -26,11 +26,14 @@ package net.luckperms.api.cacheddata; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; /** * Holds cached lookup data for a given set of query options. + * + *

    All calls will account for inheritance, as well as any default data + * provided by the platform. These calls are heavily cached and are therefore + * fast.

    */ public interface CachedData { diff --git a/api/src/main/java/net/luckperms/api/cacheddata/CachedDataManager.java b/api/src/main/java/net/luckperms/api/cacheddata/CachedDataManager.java index fe52d97a2..91682fd42 100644 --- a/api/src/main/java/net/luckperms/api/cacheddata/CachedDataManager.java +++ b/api/src/main/java/net/luckperms/api/cacheddata/CachedDataManager.java @@ -30,7 +30,6 @@ import net.luckperms.api.model.group.Group; import net.luckperms.api.model.user.User; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.concurrent.CompletableFuture; @@ -39,7 +38,7 @@ * Holds cached permission and meta lookup data for a {@link PermissionHolder}. * *

    All calls will account for inheritance, as well as any default data - * provided by the platform. This calls are heavily cached and are therefore + * provided by the platform. These calls are heavily cached and are therefore * fast.

    */ public interface CachedDataManager { @@ -81,11 +80,10 @@ public interface CachedDataManager { *

    For {@link User}s, the most appropriate query options will be their * {@link ContextManager#getQueryOptions(User) current active query options} if the * corresponding player is online, and otherwise, will fallback to - * {@link ContextManager#getStaticQueryOptions() the current static query options} - * if they are offline.

    + * {@link ContextManager#getStaticQueryOptions() the current static query options}.

    * *

    For {@link Group}s, the most appropriate query options will always be - * {@link ContextManager#getStaticQueryOptions()} the current static query options.

    + * {@link ContextManager#getStaticQueryOptions() the current static query options}.

    * * @return a permission data instance * @since 5.1 @@ -99,11 +97,10 @@ public interface CachedDataManager { *

    For {@link User}s, the most appropriate query options will be their * {@link ContextManager#getQueryOptions(User) current active query options} if the * corresponding player is online, and otherwise, will fallback to - * {@link ContextManager#getStaticQueryOptions() the current static query options} - * if they are offline.

    + * {@link ContextManager#getStaticQueryOptions() the current static query options}.

    * *

    For {@link Group}s, the most appropriate query options will always be - * {@link ContextManager#getStaticQueryOptions()} the current static query options.

    + * {@link ContextManager#getStaticQueryOptions() the current static query options}.

    * * @return a meta data instance * @since 5.1 @@ -118,7 +115,7 @@ public interface CachedDataManager { void invalidate(); /** - * Invalidates all of the underlying Permission calculators. + * Invalidates all underlying permission calculators. * *

    Can be called to allow for an update in defaults.

    */ diff --git a/api/src/main/java/net/luckperms/api/cacheddata/CachedMetaData.java b/api/src/main/java/net/luckperms/api/cacheddata/CachedMetaData.java index 646fb6f34..7e3d28850 100644 --- a/api/src/main/java/net/luckperms/api/cacheddata/CachedMetaData.java +++ b/api/src/main/java/net/luckperms/api/cacheddata/CachedMetaData.java @@ -26,7 +26,10 @@ package net.luckperms.api.cacheddata; import net.luckperms.api.metastacking.MetaStackDefinition; - +import net.luckperms.api.node.types.MetaNode; +import net.luckperms.api.node.types.PrefixNode; +import net.luckperms.api.node.types.SuffixNode; +import net.luckperms.api.node.types.WeightNode; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -39,16 +42,40 @@ /** * Holds cached meta lookup data for a specific set of contexts. + * + *

    Meta data refers to {@link PrefixNode prefixes}, {@link SuffixNode suffixes} and + * {@link MetaNode meta (options)} held by a permission holder.

    + * + *

    All calls will account for inheritance, as well as any default data + * provided by the platform. These calls are heavily cached and are therefore + * fast.

    */ public interface CachedMetaData extends CachedData { + /** + * Query a meta value for the given {@code key}. + * + *

    This method will always return a {@link Result}, but the + * {@link Result#result() inner result} {@link String} will be null if a value + * for the given key was not found.

    + * + * @param key the key + * @return a result containing the value + * @since 5.4 + */ + @NonNull Result queryMetaValue(@NonNull String key); + /** * Gets a value for the given meta key. + * + *

    If no such meta value exists for the given key, {@code null} is returned.

    * * @param key the key * @return the value */ - @Nullable String getMetaValue(@NonNull String key); + default @Nullable String getMetaValue(@NonNull String key) { + return queryMetaValue(key).result(); + } /** * Gets a value for the given meta key, and runs it through the given {@code transformer}. @@ -81,37 +108,121 @@ public interface CachedMetaData extends CachedData { } /** - * Gets the holder's highest priority prefix, or null if the holder has no prefixes + * Query for a prefix. + * + *

    This method uses the rules defined by the {@link #getPrefixStackDefinition() prefix stack} + * to produce a {@link String} output.

    + * + *

    Assuming the default configuration is used, this will usually be the value of the + * holder's highest priority prefix node.

    + * + *

    This method will always return a {@link Result}, but the + * {@link Result#result() inner result} {@link String} will be null if + * a the resultant prefix stack contained no elements.

    + * + * @return a result containing the prefix + * @since 5.4 + */ + @NonNull Result queryPrefix(); + + /** + * Gets the prefix. + * + *

    This method uses the rules defined by the {@link #getPrefixStackDefinition() prefix stack} + * to produce a {@link String} output.

    + * + *

    Assuming the default configuration is used, this will usually be the value of the + * holder's highest priority prefix node.

    + * + *

    If the resultant prefix stack contained no elements, {@code null} is returned.

    * * @return a prefix string, or null */ - @Nullable String getPrefix(); + default @Nullable String getPrefix() { + return queryPrefix().result(); + } + + /** + * Query for a suffix. + * + *

    This method uses the rules defined by the {@link #getSuffixStackDefinition() suffix stack} + * to produce a {@link String} output.

    + * + *

    Assuming the default configuration is used, this will usually be the value of the + * holder's highest priority suffix node.

    + * + *

    This method will always return a {@link Result}, but the + * {@link Result#result() inner result} {@link String} will be null if + * a the resultant suffix stack contained no elements.

    + * + * @return a result containing the suffix + * @since 5.4 + */ + @NonNull Result querySuffix(); /** - * Gets the holder's highest priority suffix, or null if the holder has no suffixes + * Gets the suffix. + * + *

    This method uses the rules defined by the {@link #getSuffixStackDefinition() suffix stack} + * to produce a {@link String} output.

    + * + *

    Assuming the default configuration is used, this will usually be the value of the + * holder's highest priority suffix node.

    + * + *

    If the resultant suffix stack contained no elements, {@code null} is returned.

    * * @return a suffix string, or null */ - @Nullable String getSuffix(); + default @Nullable String getSuffix() { + return querySuffix().result(); + } /** - * Gets an immutable copy of the meta this holder has. + * Query for a weight. * - * @return an immutable map of meta + *

    This method will always return a {@link Result}, and the + * {@link Result#result() inner result} {@link Integer} will never be null. + * A value of {@code 0} is equivalent to null.

    + * + * @return a result containing the weight + * @since 5.5 + */ + @NonNull Result queryWeight(); + + /** + * Gets the weight. + * + *

    If the there is no defined weight, {@code 0} is returned.

    + * + * @return the weight + * @since 5.5 + */ + default int getWeight() { + return queryWeight().result(); + } + + /** + * Gets a map of all accumulated {@link MetaNode meta}. + * + *

    Prefer using the {@link #getMetaValue(String)} method for querying values.

    + * + * @return a map of meta */ @NonNull @Unmodifiable Map> getMeta(); /** - * Gets an immutable sorted map of all of the prefixes the holder has, whereby the first - * value is the highest priority prefix. + * Gets a sorted map of all accumulated {@link PrefixNode prefixes}. + * + *

    Prefer using the {@link #getPrefix()} method for querying.

    * * @return a sorted map of prefixes */ @NonNull @Unmodifiable SortedMap getPrefixes(); /** - * Gets an immutable sorted map of all of the suffixes the holder has, whereby the first - * value is the highest priority suffix. + * Gets a sorted map of all accumulated {@link SuffixNode suffixes}. + * + *

    Prefer using the {@link #getSuffix()} method for querying.

    * * @return a sorted map of suffixes */ @@ -128,14 +239,14 @@ public interface CachedMetaData extends CachedData { @Nullable String getPrimaryGroup(); /** - * Gets the definition used for the prefix stack + * Gets the definition used for the prefix stack. * * @return the definition used for the prefix stack */ @NonNull MetaStackDefinition getPrefixStackDefinition(); /** - * Gets the definition used for the suffix stack + * Gets the definition used for the suffix stack. * * @return the definition used for the suffix stack */ diff --git a/api/src/main/java/net/luckperms/api/cacheddata/CachedPermissionData.java b/api/src/main/java/net/luckperms/api/cacheddata/CachedPermissionData.java index 2b8e65aa8..0f5205540 100644 --- a/api/src/main/java/net/luckperms/api/cacheddata/CachedPermissionData.java +++ b/api/src/main/java/net/luckperms/api/cacheddata/CachedPermissionData.java @@ -25,8 +25,8 @@ package net.luckperms.api.cacheddata; +import net.luckperms.api.node.Node; import net.luckperms.api.util.Tristate; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; @@ -34,17 +34,39 @@ /** * Holds cached permission lookup data for a specific set of contexts. + * + *

    All calls will account for inheritance, as well as any default data + * provided by the platform. These calls are heavily cached and are therefore + * fast.

    */ public interface CachedPermissionData extends CachedData { /** - * Gets a permission check result for the given permission node. + * Performs a permission check for the given {@code permission} node. + * + *

    This check is equivalent to the "hasPermission" method call on most platforms. + * You can use {@link Tristate#asBoolean()} if you need a truthy result.

    + * + * @param permission the permission node + * @return a result containing the tristate + * @throws NullPointerException if permission is null + * @since 5.4 + */ + @NonNull Result queryPermission(@NonNull String permission); + + /** + * Performs a permission check for the given {@code permission} node. + * + *

    This check is equivalent to the "hasPermission" method call on most platforms. + * You can use {@link Tristate#asBoolean()} if you need a truthy result.

    * * @param permission the permission node * @return a tristate result * @throws NullPointerException if permission is null */ - @NonNull Tristate checkPermission(@NonNull String permission); + default @NonNull Tristate checkPermission(@NonNull String permission) { + return queryPermission(permission).result(); + } /** * Invalidates the underlying permission calculator cache. diff --git a/api/src/main/java/net/luckperms/api/cacheddata/Result.java b/api/src/main/java/net/luckperms/api/cacheddata/Result.java new file mode 100644 index 000000000..729c6d788 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/cacheddata/Result.java @@ -0,0 +1,81 @@ +/* + * 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 net.luckperms.api.cacheddata; + +import net.luckperms.api.node.Node; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Represents the result of a cached data lookup. + * + *

    You can find "the holder that has the node that caused this result" + * using the following code:

    + *

    + *
    + *
    + * public static {@link net.luckperms.api.model.PermissionHolder.Identifier} holderThatHasTheNodeThatCausedTheResult(Result<?, ?> result) {
    + *     {@link Node} node = result.node();
    + *     if (node == null) {
    + *         return null;
    + *     }
    + *     {@link net.luckperms.api.node.metadata.types.InheritanceOriginMetadata} origin = node.getMetadata(InheritanceOriginMetadata.KEY).orElse(null);
    + *     if (origin == null) {
    + *         return null;
    + *     }
    + *     return origin.getOrigin();
    + * }
    + * 
    + * + *

    Combined with the node itself, this is all the information needed to determine + * the root cause of the result.

    + *
    + * + *

    The nullability of {@link #result()} is purposely undefined to allow the + * flexibility for methods using {@link Result} to declare it. In general, if the {@code T} type + * has a nullable/undefined value, then the return of {@link #result()} will be non-null, + * and if not, it will be nullable.

    + * + * @param the result type + * @param the node type + * @since 5.4 + */ +public interface Result { + + /** + * Gets the underlying result. + * + * @return the underlying result + */ + T result(); + + /** + * Gets the node that caused the result. + * + * @return the causing node + */ + @Nullable N node(); + +} diff --git a/api/src/main/java/net/luckperms/api/cacheddata/package-info.java b/api/src/main/java/net/luckperms/api/cacheddata/package-info.java index 0358e6716..ec5b4bb8c 100644 --- a/api/src/main/java/net/luckperms/api/cacheddata/package-info.java +++ b/api/src/main/java/net/luckperms/api/cacheddata/package-info.java @@ -24,7 +24,7 @@ */ /** - * CachedData lookup API for {@link net.luckperms.api.model.user.User}s and + * Caches permission checks and meta lookups for {@link net.luckperms.api.model.user.User}s and * {@link net.luckperms.api.model.group.Group}s. */ package net.luckperms.api.cacheddata; \ No newline at end of file diff --git a/api/src/main/java/net/luckperms/api/context/ContextManager.java b/api/src/main/java/net/luckperms/api/context/ContextManager.java index 5a6d929f9..7719caeb1 100644 --- a/api/src/main/java/net/luckperms/api/context/ContextManager.java +++ b/api/src/main/java/net/luckperms/api/context/ContextManager.java @@ -28,7 +28,6 @@ import net.luckperms.api.model.user.User; import net.luckperms.api.query.QueryMode; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.Internal; diff --git a/api/src/main/java/net/luckperms/api/context/ImmutableContextSet.java b/api/src/main/java/net/luckperms/api/context/ImmutableContextSet.java index b2312fa52..ae2fb0230 100644 --- a/api/src/main/java/net/luckperms/api/context/ImmutableContextSet.java +++ b/api/src/main/java/net/luckperms/api/context/ImmutableContextSet.java @@ -26,7 +26,6 @@ package net.luckperms.api.context; import net.luckperms.api.LuckPermsProvider; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; @@ -119,7 +118,7 @@ interface Builder { } /** - * Adds of of the contexts in another {@link ContextSet} to the set. + * Adds all the contexts in another {@link ContextSet} to the set. * * @param contextSet the set to add from * @return the builder diff --git a/api/src/main/java/net/luckperms/api/context/MutableContextSet.java b/api/src/main/java/net/luckperms/api/context/MutableContextSet.java index 83bc91384..cdee00145 100644 --- a/api/src/main/java/net/luckperms/api/context/MutableContextSet.java +++ b/api/src/main/java/net/luckperms/api/context/MutableContextSet.java @@ -26,7 +26,6 @@ package net.luckperms.api.context; import net.luckperms.api.LuckPermsProvider; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; @@ -94,7 +93,7 @@ default void addAll(@NonNull Iterable iterable) { } /** - * Adds of of the contexts in another {@link ContextSet} to this set. + * Adds all the contexts in another {@link ContextSet} to this set. * * @param contextSet the set to add from * @throws NullPointerException if the contextSet is null diff --git a/api/src/main/java/net/luckperms/api/event/LuckPermsEvent.java b/api/src/main/java/net/luckperms/api/event/LuckPermsEvent.java index 9620ebc56..a3f2cd497 100644 --- a/api/src/main/java/net/luckperms/api/event/LuckPermsEvent.java +++ b/api/src/main/java/net/luckperms/api/event/LuckPermsEvent.java @@ -26,7 +26,6 @@ package net.luckperms.api.event; import net.luckperms.api.LuckPerms; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/context/ContextUpdateEvent.java b/api/src/main/java/net/luckperms/api/event/context/ContextUpdateEvent.java index 8d35fff29..aeac27492 100644 --- a/api/src/main/java/net/luckperms/api/event/context/ContextUpdateEvent.java +++ b/api/src/main/java/net/luckperms/api/event/context/ContextUpdateEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.context.ContextManager; import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Optional; diff --git a/api/src/main/java/net/luckperms/api/event/extension/ExtensionLoadEvent.java b/api/src/main/java/net/luckperms/api/event/extension/ExtensionLoadEvent.java index 463326e6f..0097fb1a4 100644 --- a/api/src/main/java/net/luckperms/api/event/extension/ExtensionLoadEvent.java +++ b/api/src/main/java/net/luckperms/api/event/extension/ExtensionLoadEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; import net.luckperms.api.extension.Extension; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/group/GroupCacheLoadEvent.java b/api/src/main/java/net/luckperms/api/event/group/GroupCacheLoadEvent.java index 7cb693049..b2ead950a 100644 --- a/api/src/main/java/net/luckperms/api/event/group/GroupCacheLoadEvent.java +++ b/api/src/main/java/net/luckperms/api/event/group/GroupCacheLoadEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; import net.luckperms.api.model.group.Group; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/group/GroupCreateEvent.java b/api/src/main/java/net/luckperms/api/event/group/GroupCreateEvent.java index b4ff36054..3f1fad1ab 100644 --- a/api/src/main/java/net/luckperms/api/event/group/GroupCreateEvent.java +++ b/api/src/main/java/net/luckperms/api/event/group/GroupCreateEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.event.util.Param; import net.luckperms.api.model.group.Group; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/group/GroupDataRecalculateEvent.java b/api/src/main/java/net/luckperms/api/event/group/GroupDataRecalculateEvent.java index 47f261f1a..182581adc 100644 --- a/api/src/main/java/net/luckperms/api/event/group/GroupDataRecalculateEvent.java +++ b/api/src/main/java/net/luckperms/api/event/group/GroupDataRecalculateEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; import net.luckperms.api.model.group.Group; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/group/GroupDeleteEvent.java b/api/src/main/java/net/luckperms/api/event/group/GroupDeleteEvent.java index 4816dddfe..4511ca7df 100644 --- a/api/src/main/java/net/luckperms/api/event/group/GroupDeleteEvent.java +++ b/api/src/main/java/net/luckperms/api/event/group/GroupDeleteEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.cause.DeletionCause; import net.luckperms.api.event.util.Param; import net.luckperms.api.node.Node; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Set; diff --git a/api/src/main/java/net/luckperms/api/event/group/GroupLoadEvent.java b/api/src/main/java/net/luckperms/api/event/group/GroupLoadEvent.java index b4c0f0454..b8c2de247 100644 --- a/api/src/main/java/net/luckperms/api/event/group/GroupLoadEvent.java +++ b/api/src/main/java/net/luckperms/api/event/group/GroupLoadEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; import net.luckperms.api.model.group.Group; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/log/LogBroadcastEvent.java b/api/src/main/java/net/luckperms/api/event/log/LogBroadcastEvent.java index 9e263e5ed..b97c33075 100644 --- a/api/src/main/java/net/luckperms/api/event/log/LogBroadcastEvent.java +++ b/api/src/main/java/net/luckperms/api/event/log/LogBroadcastEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.type.Cancellable; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/log/LogNetworkPublishEvent.java b/api/src/main/java/net/luckperms/api/event/log/LogNetworkPublishEvent.java index b376e877d..6f2d90a23 100644 --- a/api/src/main/java/net/luckperms/api/event/log/LogNetworkPublishEvent.java +++ b/api/src/main/java/net/luckperms/api/event/log/LogNetworkPublishEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.type.Cancellable; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.UUID; diff --git a/api/src/main/java/net/luckperms/api/event/log/LogNotifyEvent.java b/api/src/main/java/net/luckperms/api/event/log/LogNotifyEvent.java index fd23a0c95..c51549348 100644 --- a/api/src/main/java/net/luckperms/api/event/log/LogNotifyEvent.java +++ b/api/src/main/java/net/luckperms/api/event/log/LogNotifyEvent.java @@ -30,7 +30,6 @@ import net.luckperms.api.event.type.Cancellable; import net.luckperms.api.event.util.Param; import net.luckperms.api.platform.PlatformEntity; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/log/LogPublishEvent.java b/api/src/main/java/net/luckperms/api/event/log/LogPublishEvent.java index 2cc247747..060cd1aef 100644 --- a/api/src/main/java/net/luckperms/api/event/log/LogPublishEvent.java +++ b/api/src/main/java/net/luckperms/api/event/log/LogPublishEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.type.Cancellable; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/log/LogReceiveEvent.java b/api/src/main/java/net/luckperms/api/event/log/LogReceiveEvent.java index 5cc4bc6d2..96ac1d9ea 100644 --- a/api/src/main/java/net/luckperms/api/event/log/LogReceiveEvent.java +++ b/api/src/main/java/net/luckperms/api/event/log/LogReceiveEvent.java @@ -28,13 +28,16 @@ import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.UUID; /** - * Called when a log entry is received via the MessagingService + * Called when a log entry is received via the MessagingService. + * + *

    Note: listening to this event is the same as listening to the {@link LogBroadcastEvent} + * and filtering for {@link LogBroadcastEvent#getOrigin() origin} = + * {@link net.luckperms.api.event.log.LogBroadcastEvent.Origin#REMOTE REMOTE}.

    */ public interface LogReceiveEvent extends LuckPermsEvent { diff --git a/api/src/main/java/net/luckperms/api/event/messaging/CustomMessageReceiveEvent.java b/api/src/main/java/net/luckperms/api/event/messaging/CustomMessageReceiveEvent.java new file mode 100644 index 000000000..2e43b5dea --- /dev/null +++ b/api/src/main/java/net/luckperms/api/event/messaging/CustomMessageReceiveEvent.java @@ -0,0 +1,59 @@ +/* + * 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 net.luckperms.api.event.messaging; + +import net.luckperms.api.event.LuckPermsEvent; +import net.luckperms.api.event.util.Param; +import net.luckperms.api.messaging.MessagingService; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Called when a custom payload message is received via the {@link MessagingService}. + * + *

    This event is effectively the 'other end' of + * {@link MessagingService#sendCustomMessage(String, String)}.

    + * + * @since 5.5 + */ +public interface CustomMessageReceiveEvent extends LuckPermsEvent { + + /** + * Gets the channel id. + * + * @return the channel id + */ + @Param(0) + @NonNull String getChannelId(); + + /** + * Gets the custom payload that was sent. + * + * @return the custom payload + */ + @Param(1) + @NonNull String getPayload(); + +} diff --git a/api/src/main/java/net/luckperms/api/event/messaging/package-info.java b/api/src/main/java/net/luckperms/api/event/messaging/package-info.java new file mode 100644 index 000000000..ed66949e5 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/event/messaging/package-info.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** + * Events relating to the {@link net.luckperms.api.messaging.MessagingService}. + */ +package net.luckperms.api.event.messaging; \ No newline at end of file diff --git a/api/src/main/java/net/luckperms/api/event/node/NodeAddEvent.java b/api/src/main/java/net/luckperms/api/event/node/NodeAddEvent.java index 96f1f99c3..c86985c15 100644 --- a/api/src/main/java/net/luckperms/api/event/node/NodeAddEvent.java +++ b/api/src/main/java/net/luckperms/api/event/node/NodeAddEvent.java @@ -27,7 +27,6 @@ import net.luckperms.api.event.util.Param; import net.luckperms.api.node.Node; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/event/node/NodeClearEvent.java b/api/src/main/java/net/luckperms/api/event/node/NodeClearEvent.java index fb6ec7b3f..1bb8e62ce 100644 --- a/api/src/main/java/net/luckperms/api/event/node/NodeClearEvent.java +++ b/api/src/main/java/net/luckperms/api/event/node/NodeClearEvent.java @@ -27,7 +27,6 @@ import net.luckperms.api.event.util.Param; import net.luckperms.api.node.Node; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/event/node/NodeMutateEvent.java b/api/src/main/java/net/luckperms/api/event/node/NodeMutateEvent.java index 42fd02751..f65575695 100644 --- a/api/src/main/java/net/luckperms/api/event/node/NodeMutateEvent.java +++ b/api/src/main/java/net/luckperms/api/event/node/NodeMutateEvent.java @@ -32,7 +32,6 @@ import net.luckperms.api.model.group.Group; import net.luckperms.api.model.user.User; import net.luckperms.api.node.Node; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/event/node/NodeRemoveEvent.java b/api/src/main/java/net/luckperms/api/event/node/NodeRemoveEvent.java index d53fa2d2a..fc29ff894 100644 --- a/api/src/main/java/net/luckperms/api/event/node/NodeRemoveEvent.java +++ b/api/src/main/java/net/luckperms/api/event/node/NodeRemoveEvent.java @@ -27,7 +27,6 @@ import net.luckperms.api.event.util.Param; import net.luckperms.api.node.Node; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/event/player/PlayerDataSaveEvent.java b/api/src/main/java/net/luckperms/api/event/player/PlayerDataSaveEvent.java index 9ccd42e23..dd147f340 100644 --- a/api/src/main/java/net/luckperms/api/event/player/PlayerDataSaveEvent.java +++ b/api/src/main/java/net/luckperms/api/event/player/PlayerDataSaveEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.util.Param; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.model.user.UserManager; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.UUID; diff --git a/api/src/main/java/net/luckperms/api/event/player/PlayerLoginProcessEvent.java b/api/src/main/java/net/luckperms/api/event/player/PlayerLoginProcessEvent.java index ea37aacdd..334f63007 100644 --- a/api/src/main/java/net/luckperms/api/event/player/PlayerLoginProcessEvent.java +++ b/api/src/main/java/net/luckperms/api/event/player/PlayerLoginProcessEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.util.Param; import net.luckperms.api.model.user.User; import net.luckperms.api.util.Result; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/api/src/main/java/net/luckperms/api/event/player/lookup/UniqueIdDetermineTypeEvent.java b/api/src/main/java/net/luckperms/api/event/player/lookup/UniqueIdDetermineTypeEvent.java index 78bec2de9..ac1ca8510 100644 --- a/api/src/main/java/net/luckperms/api/event/player/lookup/UniqueIdDetermineTypeEvent.java +++ b/api/src/main/java/net/luckperms/api/event/player/lookup/UniqueIdDetermineTypeEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.type.ResultEvent; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; @@ -56,6 +55,22 @@ public interface UniqueIdDetermineTypeEvent extends LuckPermsEvent, ResultEvent< */ String TYPE_UNAUTHENTICATED = "unauthenticated"; + /** + * The players UUID most likely belongs to a NPC (non-player character). + * + *

    Usually indicated by the UUID being {@link UUID#version() version} 2.

    + * + * @since 5.4 + */ + String TYPE_NPC = "npc"; + + /** + * Unknown UUID type. + * + * @since 5.4 + */ + String TYPE_UNKNOWN = "unknown"; + /** * Gets the {@link UUID unique id} being queried. * diff --git a/api/src/main/java/net/luckperms/api/event/player/lookup/UniqueIdLookupEvent.java b/api/src/main/java/net/luckperms/api/event/player/lookup/UniqueIdLookupEvent.java index e351926f3..d6d9d151e 100644 --- a/api/src/main/java/net/luckperms/api/event/player/lookup/UniqueIdLookupEvent.java +++ b/api/src/main/java/net/luckperms/api/event/player/lookup/UniqueIdLookupEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.type.ResultEvent; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/api/src/main/java/net/luckperms/api/event/player/lookup/UsernameLookupEvent.java b/api/src/main/java/net/luckperms/api/event/player/lookup/UsernameLookupEvent.java index 4650f080f..54eb0f625 100644 --- a/api/src/main/java/net/luckperms/api/event/player/lookup/UsernameLookupEvent.java +++ b/api/src/main/java/net/luckperms/api/event/player/lookup/UsernameLookupEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.type.ResultEvent; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/api/src/main/java/net/luckperms/api/event/player/lookup/UsernameValidityCheckEvent.java b/api/src/main/java/net/luckperms/api/event/player/lookup/UsernameValidityCheckEvent.java index 94b7444d7..a96f16875 100644 --- a/api/src/main/java/net/luckperms/api/event/player/lookup/UsernameValidityCheckEvent.java +++ b/api/src/main/java/net/luckperms/api/event/player/lookup/UsernameValidityCheckEvent.java @@ -27,7 +27,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/api/src/main/java/net/luckperms/api/event/source/EntitySource.java b/api/src/main/java/net/luckperms/api/event/source/EntitySource.java index 0cd02792f..4225ad611 100644 --- a/api/src/main/java/net/luckperms/api/event/source/EntitySource.java +++ b/api/src/main/java/net/luckperms/api/event/source/EntitySource.java @@ -26,7 +26,6 @@ package net.luckperms.api.event.source; import net.luckperms.api.platform.PlatformEntity; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/source/Source.java b/api/src/main/java/net/luckperms/api/event/source/Source.java index 2bd90bc1a..9426e870c 100644 --- a/api/src/main/java/net/luckperms/api/event/source/Source.java +++ b/api/src/main/java/net/luckperms/api/event/source/Source.java @@ -26,7 +26,6 @@ package net.luckperms.api.event.source; import net.luckperms.api.platform.PlatformEntity; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/sync/PostNetworkSyncEvent.java b/api/src/main/java/net/luckperms/api/event/sync/PostNetworkSyncEvent.java new file mode 100644 index 000000000..c156eb0ce --- /dev/null +++ b/api/src/main/java/net/luckperms/api/event/sync/PostNetworkSyncEvent.java @@ -0,0 +1,81 @@ +/* + * 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 net.luckperms.api.event.sync; + +import net.luckperms.api.event.LuckPermsEvent; +import net.luckperms.api.event.util.Param; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.UUID; + +/** + * Called after a network synchronisation task has been completed. + * + *

    Note: the generic {@link PostSyncEvent} will also be called for {@link SyncType#FULL full syncs}.

    + * + * @since 5.5 + */ +public interface PostNetworkSyncEvent extends LuckPermsEvent { + + /** + * Gets the ID of the sync request + * + * @return the id of the sync request + */ + @Param(0) + @NonNull UUID getSyncId(); + + /** + * Gets the sync type. + * + * @return the sync type + */ + @Param(1) + @NonNull SyncType getType(); + + /** + * Gets if a sync occurred. + * + *

    For {@link SyncType} = {@link SyncType#FULL FULL}, this method always returns true.

    + * + *

    For {@link SyncType} = {@link SyncType#SPECIFIC_USER SPECIFIC_USER}, this method returns true if the + * user in question was online/loaded in memory at the time, and false otherwise.

    + * + * @return if a sync occurred + */ + @Param(2) + boolean didSyncOccur(); + + /** + * Gets the unique id of the specific user that has been synced, if applicable. + * + * @return the unique id of the specific user + */ + @Param(3) + @Nullable UUID getSpecificUserUniqueId(); + +} diff --git a/api/src/main/java/net/luckperms/api/event/sync/PostSyncEvent.java b/api/src/main/java/net/luckperms/api/event/sync/PostSyncEvent.java index 0a048ca04..281b6429f 100644 --- a/api/src/main/java/net/luckperms/api/event/sync/PostSyncEvent.java +++ b/api/src/main/java/net/luckperms/api/event/sync/PostSyncEvent.java @@ -28,7 +28,10 @@ import net.luckperms.api.event.LuckPermsEvent; /** - * Called when an sync task has been completed + * Called after a full synchronisation task has been completed. + * + *

    Note: this event is also called after synchronisations that were triggered over the network. + * In other words, this event will be called in addition to {@link PostNetworkSyncEvent}.

    */ public interface PostSyncEvent extends LuckPermsEvent { diff --git a/api/src/main/java/net/luckperms/api/event/sync/PreNetworkSyncEvent.java b/api/src/main/java/net/luckperms/api/event/sync/PreNetworkSyncEvent.java index eac09f9b0..013d47f39 100644 --- a/api/src/main/java/net/luckperms/api/event/sync/PreNetworkSyncEvent.java +++ b/api/src/main/java/net/luckperms/api/event/sync/PreNetworkSyncEvent.java @@ -28,13 +28,16 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.type.Cancellable; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.UUID; /** - * Called before a received network sync task runs + * Called after a request for synchronisation has been received via the messaging service, + * but before it has actually been completed. + * + *

    Note: the generic {@link PreSyncEvent} will also be called for {@link SyncType#FULL full syncs}.

    */ public interface PreNetworkSyncEvent extends LuckPermsEvent, Cancellable { @@ -46,4 +49,22 @@ public interface PreNetworkSyncEvent extends LuckPermsEvent, Cancellable { @Param(0) @NonNull UUID getSyncId(); + /** + * Gets the sync type. + * + * @return the sync type + * @since 5.5 + */ + @Param(1) + @NonNull SyncType getType(); + + /** + * Gets the unique id of the specific user that will be synced, if applicable. + * + * @return the unique id of the specific user + * @since 5.5 + */ + @Param(2) + @Nullable UUID getSpecificUserUniqueId(); + } diff --git a/api/src/main/java/net/luckperms/api/event/sync/PreSyncEvent.java b/api/src/main/java/net/luckperms/api/event/sync/PreSyncEvent.java index 41eb5caf9..3961fe443 100644 --- a/api/src/main/java/net/luckperms/api/event/sync/PreSyncEvent.java +++ b/api/src/main/java/net/luckperms/api/event/sync/PreSyncEvent.java @@ -29,7 +29,10 @@ import net.luckperms.api.event.type.Cancellable; /** - * Called before a sync task runs + * Called just before a full synchronisation task runs. + * + *

    Note: this event is also called before synchronisations that were triggered over the network. + * In other words, this event will be called in addition to {@link PreNetworkSyncEvent}.

    */ public interface PreSyncEvent extends LuckPermsEvent, Cancellable { diff --git a/sponge/sponge-service-api6/src/main/java/org/spongepowered/api/service/permission/SubjectReference.java b/api/src/main/java/net/luckperms/api/event/sync/SyncType.java similarity index 76% rename from sponge/sponge-service-api6/src/main/java/org/spongepowered/api/service/permission/SubjectReference.java rename to api/src/main/java/net/luckperms/api/event/sync/SyncType.java index 03929c5a5..14f7ef0c1 100644 --- a/sponge/sponge-service-api6/src/main/java/org/spongepowered/api/service/permission/SubjectReference.java +++ b/api/src/main/java/net/luckperms/api/event/sync/SyncType.java @@ -23,20 +23,23 @@ * SOFTWARE. */ -package org.spongepowered.api.service.permission; - -import java.util.concurrent.CompletableFuture; +package net.luckperms.api.event.sync; /** - * This is included, as the interface didn't exist in API6. - * We just shade it into the LP jar so we can still implement it. :) + * Represents the type of synchronisation task. + * + * @since 5.5 */ -public interface SubjectReference { - - String getCollectionIdentifier(); +public enum SyncType { - String getSubjectIdentifier(); + /** + * A full sync will be performed - all groups, users and tracks + */ + FULL, - CompletableFuture resolve(); + /** + * Only a specific user will be synced + */ + SPECIFIC_USER -} \ No newline at end of file +} diff --git a/api/src/main/java/net/luckperms/api/event/track/TrackCreateEvent.java b/api/src/main/java/net/luckperms/api/event/track/TrackCreateEvent.java index 411320c56..3075e56f7 100644 --- a/api/src/main/java/net/luckperms/api/event/track/TrackCreateEvent.java +++ b/api/src/main/java/net/luckperms/api/event/track/TrackCreateEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.event.util.Param; import net.luckperms.api.track.Track; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/track/TrackDeleteEvent.java b/api/src/main/java/net/luckperms/api/event/track/TrackDeleteEvent.java index cdfb2316f..24c6ee0f7 100644 --- a/api/src/main/java/net/luckperms/api/event/track/TrackDeleteEvent.java +++ b/api/src/main/java/net/luckperms/api/event/track/TrackDeleteEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.cause.DeletionCause; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; diff --git a/api/src/main/java/net/luckperms/api/event/track/TrackLoadEvent.java b/api/src/main/java/net/luckperms/api/event/track/TrackLoadEvent.java index 318a7d201..78d42e273 100644 --- a/api/src/main/java/net/luckperms/api/event/track/TrackLoadEvent.java +++ b/api/src/main/java/net/luckperms/api/event/track/TrackLoadEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; import net.luckperms.api.track.Track; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/track/mutate/TrackAddGroupEvent.java b/api/src/main/java/net/luckperms/api/event/track/mutate/TrackAddGroupEvent.java index 3657e9779..14306b4ad 100644 --- a/api/src/main/java/net/luckperms/api/event/track/mutate/TrackAddGroupEvent.java +++ b/api/src/main/java/net/luckperms/api/event/track/mutate/TrackAddGroupEvent.java @@ -26,7 +26,6 @@ package net.luckperms.api.event.track.mutate; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/track/mutate/TrackMutateEvent.java b/api/src/main/java/net/luckperms/api/event/track/mutate/TrackMutateEvent.java index 88f6b0445..276778707 100644 --- a/api/src/main/java/net/luckperms/api/event/track/mutate/TrackMutateEvent.java +++ b/api/src/main/java/net/luckperms/api/event/track/mutate/TrackMutateEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; import net.luckperms.api.track.Track; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/event/track/mutate/TrackRemoveGroupEvent.java b/api/src/main/java/net/luckperms/api/event/track/mutate/TrackRemoveGroupEvent.java index 6d985a2dc..edf93e99f 100644 --- a/api/src/main/java/net/luckperms/api/event/track/mutate/TrackRemoveGroupEvent.java +++ b/api/src/main/java/net/luckperms/api/event/track/mutate/TrackRemoveGroupEvent.java @@ -26,7 +26,6 @@ package net.luckperms.api.event.track.mutate; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/type/Cancellable.java b/api/src/main/java/net/luckperms/api/event/type/Cancellable.java index 8b665308b..4091406d0 100644 --- a/api/src/main/java/net/luckperms/api/event/type/Cancellable.java +++ b/api/src/main/java/net/luckperms/api/event/type/Cancellable.java @@ -26,7 +26,6 @@ package net.luckperms.api.event.type; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/api/src/main/java/net/luckperms/api/event/type/ResultEvent.java b/api/src/main/java/net/luckperms/api/event/type/ResultEvent.java index 685702a6a..5f2a951ff 100644 --- a/api/src/main/java/net/luckperms/api/event/type/ResultEvent.java +++ b/api/src/main/java/net/luckperms/api/event/type/ResultEvent.java @@ -26,7 +26,6 @@ package net.luckperms.api.event.type; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.concurrent.atomic.AtomicReference; diff --git a/api/src/main/java/net/luckperms/api/event/type/Sourced.java b/api/src/main/java/net/luckperms/api/event/type/Sourced.java index e3724efe3..407d56d1a 100644 --- a/api/src/main/java/net/luckperms/api/event/type/Sourced.java +++ b/api/src/main/java/net/luckperms/api/event/type/Sourced.java @@ -27,7 +27,6 @@ import net.luckperms.api.event.source.Source; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/user/UserCacheLoadEvent.java b/api/src/main/java/net/luckperms/api/event/user/UserCacheLoadEvent.java index 9aa312aff..363df3629 100644 --- a/api/src/main/java/net/luckperms/api/event/user/UserCacheLoadEvent.java +++ b/api/src/main/java/net/luckperms/api/event/user/UserCacheLoadEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; import net.luckperms.api.model.user.User; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/user/UserDataRecalculateEvent.java b/api/src/main/java/net/luckperms/api/event/user/UserDataRecalculateEvent.java index e9f0b59a7..fbbc72b24 100644 --- a/api/src/main/java/net/luckperms/api/event/user/UserDataRecalculateEvent.java +++ b/api/src/main/java/net/luckperms/api/event/user/UserDataRecalculateEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; import net.luckperms.api.model.user.User; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/user/UserFirstLoginEvent.java b/api/src/main/java/net/luckperms/api/event/user/UserFirstLoginEvent.java index c1157e233..7f656ed1c 100644 --- a/api/src/main/java/net/luckperms/api/event/user/UserFirstLoginEvent.java +++ b/api/src/main/java/net/luckperms/api/event/user/UserFirstLoginEvent.java @@ -27,7 +27,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.UUID; diff --git a/api/src/main/java/net/luckperms/api/event/user/UserLoadEvent.java b/api/src/main/java/net/luckperms/api/event/user/UserLoadEvent.java index 45418b269..8b36fdce7 100644 --- a/api/src/main/java/net/luckperms/api/event/user/UserLoadEvent.java +++ b/api/src/main/java/net/luckperms/api/event/user/UserLoadEvent.java @@ -28,7 +28,6 @@ import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.util.Param; import net.luckperms.api.model.user.User; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/user/UserUnloadEvent.java b/api/src/main/java/net/luckperms/api/event/user/UserUnloadEvent.java index 21b2ad5dc..a97c9ec63 100644 --- a/api/src/main/java/net/luckperms/api/event/user/UserUnloadEvent.java +++ b/api/src/main/java/net/luckperms/api/event/user/UserUnloadEvent.java @@ -29,7 +29,6 @@ import net.luckperms.api.event.type.Cancellable; import net.luckperms.api.event.util.Param; import net.luckperms.api.model.user.User; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/event/user/track/UserTrackEvent.java b/api/src/main/java/net/luckperms/api/event/user/track/UserTrackEvent.java index 3c6bec429..cdec0416b 100644 --- a/api/src/main/java/net/luckperms/api/event/user/track/UserTrackEvent.java +++ b/api/src/main/java/net/luckperms/api/event/user/track/UserTrackEvent.java @@ -30,7 +30,6 @@ import net.luckperms.api.event.util.Param; import net.luckperms.api.model.user.User; import net.luckperms.api.track.Track; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Optional; diff --git a/api/src/main/java/net/luckperms/api/messaging/MessagingService.java b/api/src/main/java/net/luckperms/api/messaging/MessagingService.java index d4283053b..a1fa20d55 100644 --- a/api/src/main/java/net/luckperms/api/messaging/MessagingService.java +++ b/api/src/main/java/net/luckperms/api/messaging/MessagingService.java @@ -26,12 +26,12 @@ package net.luckperms.api.messaging; import net.luckperms.api.LuckPerms; +import net.luckperms.api.event.messaging.CustomMessageReceiveEvent; import net.luckperms.api.model.user.User; - import org.checkerframework.checker.nullness.qual.NonNull; /** - * A means to push changes to other servers using the platforms networking + * A means to send messages to other servers using the platforms networking */ public interface MessagingService { @@ -72,4 +72,44 @@ public interface MessagingService { */ void pushUserUpdate(@NonNull User user); + /** + * Uses the messaging service to send a message with a custom payload. + * + *

    The intended use case of this functionality is to allow plugins/mods + * to send lightweight and permissions-related custom messages + * between instances, piggy-backing on top of the messenger abstraction + * already built into LuckPerms.

    + * + *

    It is not intended as a full message broker replacement/abstraction. + * Note that some of the messenger implementations in LuckPerms cannot handle + * a high volume of messages being sent (for example the SQL messenger). + * Additionally, some implementations do not give any guarantees that a message + * will be delivered on time or even at all (for example the plugin message + * messengers).

    + * + *

    With all of that in mind, please consider that if you are using this + * functionality to send messages that have nothing to do with LuckPerms or + * permissions, or that require guarantees around delivery reliability, you + * are most likely misusing the API and would be better off building your own + * integration with a message broker.

    + * + *

    Whilst there is (currently) no strict validation, it is recommended + * that the channel id should use the same format as Minecraft resource locations / + * namespaced keys. For example, a plugin called "SuperRanks" sending rank-up + * notifications using custom payload messages might use the channel id + * {@code "superranks:notifications"} for this purpose.

    + * + *

    The payload can be any valid UTF-8 string.

    + * + *

    The message will be delivered asynchronously.

    + * + *

    Other LuckPerms instances that receive the message will publish it to API + * consumers using the {@link CustomMessageReceiveEvent}.

    + * + * @param channelId the channel id + * @param payload the message payload + * @since 5.5 + */ + void sendCustomMessage(@NonNull String channelId, @NonNull String payload); + } diff --git a/api/src/main/java/net/luckperms/api/messenger/IncomingMessageConsumer.java b/api/src/main/java/net/luckperms/api/messenger/IncomingMessageConsumer.java index fe6eb9324..69d59594c 100644 --- a/api/src/main/java/net/luckperms/api/messenger/IncomingMessageConsumer.java +++ b/api/src/main/java/net/luckperms/api/messenger/IncomingMessageConsumer.java @@ -27,7 +27,6 @@ import net.luckperms.api.messenger.message.Message; import net.luckperms.api.messenger.message.OutgoingMessage; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.NonExtendable; diff --git a/api/src/main/java/net/luckperms/api/messenger/Messenger.java b/api/src/main/java/net/luckperms/api/messenger/Messenger.java index 6d585f7b2..0ffade886 100644 --- a/api/src/main/java/net/luckperms/api/messenger/Messenger.java +++ b/api/src/main/java/net/luckperms/api/messenger/Messenger.java @@ -27,7 +27,6 @@ import net.luckperms.api.messenger.message.Message; import net.luckperms.api.messenger.message.OutgoingMessage; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.OverrideOnly; diff --git a/api/src/main/java/net/luckperms/api/messenger/MessengerProvider.java b/api/src/main/java/net/luckperms/api/messenger/MessengerProvider.java index a20cd7a32..31d12ec9a 100644 --- a/api/src/main/java/net/luckperms/api/messenger/MessengerProvider.java +++ b/api/src/main/java/net/luckperms/api/messenger/MessengerProvider.java @@ -26,7 +26,6 @@ package net.luckperms.api.messenger; import net.luckperms.api.LuckPerms; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.OverrideOnly; diff --git a/api/src/main/java/net/luckperms/api/messenger/message/Message.java b/api/src/main/java/net/luckperms/api/messenger/message/Message.java index edbd689af..bde57a056 100644 --- a/api/src/main/java/net/luckperms/api/messenger/message/Message.java +++ b/api/src/main/java/net/luckperms/api/messenger/message/Message.java @@ -26,7 +26,6 @@ package net.luckperms.api.messenger.message; import net.luckperms.api.messenger.Messenger; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.NonExtendable; diff --git a/api/src/main/java/net/luckperms/api/messenger/message/OutgoingMessage.java b/api/src/main/java/net/luckperms/api/messenger/message/OutgoingMessage.java index 88634a007..9bf39ef8d 100644 --- a/api/src/main/java/net/luckperms/api/messenger/message/OutgoingMessage.java +++ b/api/src/main/java/net/luckperms/api/messenger/message/OutgoingMessage.java @@ -26,7 +26,6 @@ package net.luckperms.api.messenger.message; import net.luckperms.api.messenger.IncomingMessageConsumer; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/messenger/message/type/ActionLogMessage.java b/api/src/main/java/net/luckperms/api/messenger/message/type/ActionLogMessage.java index 9cf522423..55c093e6d 100644 --- a/api/src/main/java/net/luckperms/api/messenger/message/type/ActionLogMessage.java +++ b/api/src/main/java/net/luckperms/api/messenger/message/type/ActionLogMessage.java @@ -27,7 +27,6 @@ import net.luckperms.api.actionlog.Action; import net.luckperms.api.messenger.message.Message; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/messenger/message/type/CustomMessage.java b/api/src/main/java/net/luckperms/api/messenger/message/type/CustomMessage.java new file mode 100644 index 000000000..7fb820ebe --- /dev/null +++ b/api/src/main/java/net/luckperms/api/messenger/message/type/CustomMessage.java @@ -0,0 +1,56 @@ +/* + * 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 net.luckperms.api.messenger.message.type; + +import net.luckperms.api.messenger.message.Message; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents a "custom payload" message. + * + *

    Used by API consumers to send custom messages between servers.

    + * + * @see net.luckperms.api.messaging.MessagingService#sendCustomMessage(String, String) + * @see net.luckperms.api.event.messaging.CustomMessageReceiveEvent + * @since 5.5 + */ +public interface CustomMessage extends Message { + + /** + * Gets the channel identifier. + * + * @return the namespace + */ + @NonNull String getChannelId(); + + /** + * Gets the payload. + * + * @return the payload + */ + @NonNull String getPayload(); + +} diff --git a/api/src/main/java/net/luckperms/api/messenger/message/type/UserUpdateMessage.java b/api/src/main/java/net/luckperms/api/messenger/message/type/UserUpdateMessage.java index 15db8071d..88f8c642c 100644 --- a/api/src/main/java/net/luckperms/api/messenger/message/type/UserUpdateMessage.java +++ b/api/src/main/java/net/luckperms/api/messenger/message/type/UserUpdateMessage.java @@ -26,7 +26,6 @@ package net.luckperms.api.messenger.message.type; import net.luckperms.api.messenger.message.Message; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.UUID; diff --git a/api/src/main/java/net/luckperms/api/metastacking/MetaStackDefinition.java b/api/src/main/java/net/luckperms/api/metastacking/MetaStackDefinition.java index 71140281d..0b91379c6 100644 --- a/api/src/main/java/net/luckperms/api/metastacking/MetaStackDefinition.java +++ b/api/src/main/java/net/luckperms/api/metastacking/MetaStackDefinition.java @@ -26,7 +26,6 @@ package net.luckperms.api.metastacking; import net.luckperms.api.query.OptionKey; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/metastacking/MetaStackElement.java b/api/src/main/java/net/luckperms/api/metastacking/MetaStackElement.java index 9bc6bf5ed..bb5c0248a 100644 --- a/api/src/main/java/net/luckperms/api/metastacking/MetaStackElement.java +++ b/api/src/main/java/net/luckperms/api/metastacking/MetaStackElement.java @@ -27,7 +27,6 @@ import net.luckperms.api.node.ChatMetaType; import net.luckperms.api.node.types.ChatMetaNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/api/src/main/java/net/luckperms/api/model/PermissionHolder.java b/api/src/main/java/net/luckperms/api/model/PermissionHolder.java index dd1dae4b5..831d1b6ca 100644 --- a/api/src/main/java/net/luckperms/api/model/PermissionHolder.java +++ b/api/src/main/java/net/luckperms/api/model/PermissionHolder.java @@ -35,7 +35,6 @@ import net.luckperms.api.node.NodeType; import net.luckperms.api.query.Flag; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/model/PlayerSaveResult.java b/api/src/main/java/net/luckperms/api/model/PlayerSaveResult.java index 9a9bcfd14..fd7d875ce 100644 --- a/api/src/main/java/net/luckperms/api/model/PlayerSaveResult.java +++ b/api/src/main/java/net/luckperms/api/model/PlayerSaveResult.java @@ -26,7 +26,6 @@ package net.luckperms.api.model; import net.luckperms.api.model.user.UserManager; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/model/data/DataMutateResult.java b/api/src/main/java/net/luckperms/api/model/data/DataMutateResult.java index 68676414f..39bddd54d 100644 --- a/api/src/main/java/net/luckperms/api/model/data/DataMutateResult.java +++ b/api/src/main/java/net/luckperms/api/model/data/DataMutateResult.java @@ -29,7 +29,6 @@ import net.luckperms.api.node.Node; import net.luckperms.api.track.Track; import net.luckperms.api.util.Result; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/model/data/NodeMap.java b/api/src/main/java/net/luckperms/api/model/data/NodeMap.java index abdd86135..a52498b72 100644 --- a/api/src/main/java/net/luckperms/api/model/data/NodeMap.java +++ b/api/src/main/java/net/luckperms/api/model/data/NodeMap.java @@ -35,7 +35,6 @@ import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeEqualityPredicate; import net.luckperms.api.util.Tristate; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/model/group/Group.java b/api/src/main/java/net/luckperms/api/model/group/Group.java index 1e30d2ced..17c4e7056 100644 --- a/api/src/main/java/net/luckperms/api/model/group/Group.java +++ b/api/src/main/java/net/luckperms/api/model/group/Group.java @@ -27,7 +27,6 @@ import net.luckperms.api.model.PermissionHolder; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/api/src/main/java/net/luckperms/api/model/group/GroupManager.java b/api/src/main/java/net/luckperms/api/model/group/GroupManager.java index 551a3bbfe..b7a37dbcf 100644 --- a/api/src/main/java/net/luckperms/api/model/group/GroupManager.java +++ b/api/src/main/java/net/luckperms/api/model/group/GroupManager.java @@ -28,7 +28,6 @@ import net.luckperms.api.node.HeldNode; import net.luckperms.api.node.Node; import net.luckperms.api.node.matcher.NodeMatcher; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/model/user/User.java b/api/src/main/java/net/luckperms/api/model/user/User.java index 0c3e45e6c..94b746b10 100644 --- a/api/src/main/java/net/luckperms/api/model/user/User.java +++ b/api/src/main/java/net/luckperms/api/model/user/User.java @@ -27,7 +27,6 @@ import net.luckperms.api.model.PermissionHolder; import net.luckperms.api.model.data.DataMutateResult; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/api/src/main/java/net/luckperms/api/model/user/UserManager.java b/api/src/main/java/net/luckperms/api/model/user/UserManager.java index d58336a0c..f2978fb15 100644 --- a/api/src/main/java/net/luckperms/api/model/user/UserManager.java +++ b/api/src/main/java/net/luckperms/api/model/user/UserManager.java @@ -29,7 +29,6 @@ import net.luckperms.api.node.HeldNode; import net.luckperms.api.node.Node; import net.luckperms.api.node.matcher.NodeMatcher; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -82,6 +81,16 @@ public interface UserManager { return loadUser(uniqueId, null); } + /** + * Loads multiple users from the plugin's storage provider into memory. + * + * @param uniqueIds the uuids of the users to load + * @return a future for an unmodifiable map of loaded users + * @throws NullPointerException if the uuid set is null + * @since 5.6 + */ + @NonNull CompletableFuture<@Unmodifiable Map> loadUsers(@NonNull Set<@NonNull UUID> uniqueIds); + /** * Uses the LuckPerms cache to find a uuid for the given username. * diff --git a/api/src/main/java/net/luckperms/api/node/ChatMetaType.java b/api/src/main/java/net/luckperms/api/node/ChatMetaType.java index 87a85c177..d0033a919 100644 --- a/api/src/main/java/net/luckperms/api/node/ChatMetaType.java +++ b/api/src/main/java/net/luckperms/api/node/ChatMetaType.java @@ -28,9 +28,10 @@ import net.luckperms.api.node.types.ChatMetaNode; import net.luckperms.api.node.types.PrefixNode; import net.luckperms.api.node.types.SuffixNode; - import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Locale; + /** * Represents a type of chat meta */ @@ -70,7 +71,7 @@ public enum ChatMetaType { private final NodeType> nodeType; ChatMetaType(NodeType> nodeType) { - this.name = nodeType.name().toLowerCase(); + this.name = nodeType.name().toLowerCase(Locale.ROOT); this.nodeType = nodeType; } diff --git a/api/src/main/java/net/luckperms/api/node/HeldNode.java b/api/src/main/java/net/luckperms/api/node/HeldNode.java index 55d2754d7..ed966dba2 100644 --- a/api/src/main/java/net/luckperms/api/node/HeldNode.java +++ b/api/src/main/java/net/luckperms/api/node/HeldNode.java @@ -26,7 +26,6 @@ package net.luckperms.api.node; import net.luckperms.api.model.PermissionHolder; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/node/Node.java b/api/src/main/java/net/luckperms/api/node/Node.java index e27fc98a6..881ac9750 100644 --- a/api/src/main/java/net/luckperms/api/node/Node.java +++ b/api/src/main/java/net/luckperms/api/node/Node.java @@ -36,7 +36,6 @@ import net.luckperms.api.node.types.RegexPermissionNode; import net.luckperms.api.node.types.SuffixNode; import net.luckperms.api.node.types.WeightNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.ApiStatus.NonExtendable; diff --git a/api/src/main/java/net/luckperms/api/node/NodeBuilder.java b/api/src/main/java/net/luckperms/api/node/NodeBuilder.java index 6e9e38304..9e4c9cdd7 100644 --- a/api/src/main/java/net/luckperms/api/node/NodeBuilder.java +++ b/api/src/main/java/net/luckperms/api/node/NodeBuilder.java @@ -27,7 +27,6 @@ import net.luckperms.api.context.ContextSet; import net.luckperms.api.node.metadata.NodeMetadataKey; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.ApiStatus.NonExtendable; diff --git a/api/src/main/java/net/luckperms/api/node/NodeBuilderRegistry.java b/api/src/main/java/net/luckperms/api/node/NodeBuilderRegistry.java index 83e9601da..5f2dea12b 100644 --- a/api/src/main/java/net/luckperms/api/node/NodeBuilderRegistry.java +++ b/api/src/main/java/net/luckperms/api/node/NodeBuilderRegistry.java @@ -33,7 +33,6 @@ import net.luckperms.api.node.types.RegexPermissionNode; import net.luckperms.api.node.types.SuffixNode; import net.luckperms.api.node.types.WeightNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.Internal; diff --git a/api/src/main/java/net/luckperms/api/node/NodeType.java b/api/src/main/java/net/luckperms/api/node/NodeType.java index 98d783a81..e1c7b8143 100644 --- a/api/src/main/java/net/luckperms/api/node/NodeType.java +++ b/api/src/main/java/net/luckperms/api/node/NodeType.java @@ -34,7 +34,6 @@ import net.luckperms.api.node.types.RegexPermissionNode; import net.luckperms.api.node.types.SuffixNode; import net.luckperms.api.node.types.WeightNode; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; @@ -53,7 +52,8 @@ public interface NodeType { NodeType PERMISSION = new SimpleNodeType<>( "PERMISSION", n -> n instanceof PermissionNode, - n -> (PermissionNode) n + n -> (PermissionNode) n, + 7 ); /** @@ -62,7 +62,8 @@ public interface NodeType { NodeType REGEX_PERMISSION = new SimpleNodeType<>( "REGEX_PERMISSION", n -> n instanceof RegexPermissionNode, - n -> (RegexPermissionNode) n + n -> (RegexPermissionNode) n, + 6 ); /** @@ -71,7 +72,8 @@ public interface NodeType { NodeType INHERITANCE = new SimpleNodeType<>( "INHERITANCE", n -> n instanceof InheritanceNode, - n -> (InheritanceNode) n + n -> (InheritanceNode) n, + 0 ); /** @@ -80,7 +82,8 @@ public interface NodeType { NodeType PREFIX = new SimpleNodeType<>( "PREFIX", n -> n instanceof PrefixNode, - n -> (PrefixNode) n + n -> (PrefixNode) n, + 1 ); /** @@ -89,7 +92,8 @@ public interface NodeType { NodeType SUFFIX = new SimpleNodeType<>( "SUFFIX", n -> n instanceof SuffixNode, - n -> (SuffixNode) n + n -> (SuffixNode) n, + 2 ); /** @@ -98,7 +102,8 @@ public interface NodeType { NodeType META = new SimpleNodeType<>( "META", n -> n instanceof MetaNode, - n -> (MetaNode) n + n -> (MetaNode) n, + 3 ); /** @@ -107,7 +112,8 @@ public interface NodeType { NodeType WEIGHT = new SimpleNodeType<>( "WEIGHT", n -> n instanceof WeightNode, - n -> (WeightNode) n + n -> (WeightNode) n, + 4 ); /** @@ -116,7 +122,8 @@ public interface NodeType { NodeType DISPLAY_NAME = new SimpleNodeType<>( "DISPLAY_NAME", n -> n instanceof DisplayNameNode, - n -> (DisplayNameNode) n + n -> (DisplayNameNode) n, + 5 ); /** @@ -128,7 +135,8 @@ public interface NodeType { NodeType> CHAT_META = new SimpleNodeType<>( "CHAT_META", n -> n instanceof ChatMetaNode, - n -> (ChatMetaNode) n + n -> (ChatMetaNode) n, + -1 ); /** @@ -140,7 +148,8 @@ public interface NodeType { NodeType META_OR_CHAT_META = new SimpleNodeType<>( "META_OR_CHAT_META", n -> META.matches(n) || CHAT_META.matches(n), - Function.identity() + Function.identity(), + -1 ); /** diff --git a/api/src/main/java/net/luckperms/api/node/SimpleNodeType.java b/api/src/main/java/net/luckperms/api/node/SimpleNodeType.java index 4e563f699..1e0adbb63 100644 --- a/api/src/main/java/net/luckperms/api/node/SimpleNodeType.java +++ b/api/src/main/java/net/luckperms/api/node/SimpleNodeType.java @@ -32,15 +32,17 @@ import java.util.function.Function; import java.util.function.Predicate; -final class SimpleNodeType implements NodeType { +final class SimpleNodeType implements NodeType, Comparable> { private final String name; private final Predicate matches; private final Function cast; + private final int sortOrder; - SimpleNodeType(String name, Predicate matches, Function cast) { + SimpleNodeType(String name, Predicate matches, Function cast, int sortOrder) { this.name = name; this.matches = matches; this.cast = cast; + this.sortOrder = sortOrder; } @Override @@ -66,4 +68,9 @@ public boolean matches(@NonNull Node node) { public String toString() { return name(); } + + @Override + public int compareTo(@NotNull SimpleNodeType o) { + return Integer.compare(this.sortOrder, o.sortOrder); + } } diff --git a/api/src/main/java/net/luckperms/api/node/matcher/NodeMatcher.java b/api/src/main/java/net/luckperms/api/node/matcher/NodeMatcher.java index bddb8b4c7..d08b550c9 100644 --- a/api/src/main/java/net/luckperms/api/node/matcher/NodeMatcher.java +++ b/api/src/main/java/net/luckperms/api/node/matcher/NodeMatcher.java @@ -30,7 +30,6 @@ import net.luckperms.api.node.NodeEqualityPredicate; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.MetaNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.NonExtendable; diff --git a/api/src/main/java/net/luckperms/api/node/matcher/NodeMatcherFactory.java b/api/src/main/java/net/luckperms/api/node/matcher/NodeMatcherFactory.java index a32c32ab4..3fd53b570 100644 --- a/api/src/main/java/net/luckperms/api/node/matcher/NodeMatcherFactory.java +++ b/api/src/main/java/net/luckperms/api/node/matcher/NodeMatcherFactory.java @@ -29,7 +29,6 @@ import net.luckperms.api.node.NodeEqualityPredicate; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.MetaNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.Internal; diff --git a/api/src/main/java/net/luckperms/api/node/metadata/SimpleNodeMetadataKey.java b/api/src/main/java/net/luckperms/api/node/metadata/SimpleNodeMetadataKey.java index 44a00074f..6350538ae 100644 --- a/api/src/main/java/net/luckperms/api/node/metadata/SimpleNodeMetadataKey.java +++ b/api/src/main/java/net/luckperms/api/node/metadata/SimpleNodeMetadataKey.java @@ -27,6 +27,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Locale; import java.util.Objects; final class SimpleNodeMetadataKey implements NodeMetadataKey { @@ -34,7 +35,7 @@ final class SimpleNodeMetadataKey implements NodeMetadataKey { private final Class type; SimpleNodeMetadataKey(String name, Class type) { - this.name = name.toLowerCase(); + this.name = name.toLowerCase(Locale.ROOT); this.type = type; } diff --git a/api/src/main/java/net/luckperms/api/node/metadata/types/InheritanceOriginMetadata.java b/api/src/main/java/net/luckperms/api/node/metadata/types/InheritanceOriginMetadata.java index 68b70f95b..6abe244f9 100644 --- a/api/src/main/java/net/luckperms/api/node/metadata/types/InheritanceOriginMetadata.java +++ b/api/src/main/java/net/luckperms/api/node/metadata/types/InheritanceOriginMetadata.java @@ -26,9 +26,9 @@ package net.luckperms.api.node.metadata.types; import net.luckperms.api.model.PermissionHolder; +import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; import net.luckperms.api.node.metadata.NodeMetadataKey; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.NonExtendable; @@ -56,6 +56,15 @@ public interface InheritanceOriginMetadata { */ PermissionHolder.@NonNull Identifier getOrigin(); + /** + * Gets the {@link DataType type} of the {@link net.luckperms.api.model.data.NodeMap} + * the node was inherited from. + * + * @return the type of the NodeMap the node was inherited from. + * @since 5.4 + */ + @NonNull DataType getDataType(); + /** * Gets whether the associated node was inherited from another holder. * diff --git a/api/src/main/java/net/luckperms/api/node/types/ChatMetaNode.java b/api/src/main/java/net/luckperms/api/node/types/ChatMetaNode.java index 1bbe1ddce..f7056ff64 100644 --- a/api/src/main/java/net/luckperms/api/node/types/ChatMetaNode.java +++ b/api/src/main/java/net/luckperms/api/node/types/ChatMetaNode.java @@ -28,7 +28,6 @@ import net.luckperms.api.node.ChatMetaType; import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.ScopedNode; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/node/types/DisplayNameNode.java b/api/src/main/java/net/luckperms/api/node/types/DisplayNameNode.java index 4f84be800..3db052fad 100644 --- a/api/src/main/java/net/luckperms/api/node/types/DisplayNameNode.java +++ b/api/src/main/java/net/luckperms/api/node/types/DisplayNameNode.java @@ -30,7 +30,6 @@ import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.ScopedNode; - import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -64,6 +63,7 @@ public interface DisplayNameNode extends ScopedNode { * * @param displayName the display name * @return the builder + * @throws IllegalArgumentException if {@code displayName} is empty */ @NonNull Builder displayName(@NonNull String displayName); diff --git a/api/src/main/java/net/luckperms/api/node/types/InheritanceNode.java b/api/src/main/java/net/luckperms/api/node/types/InheritanceNode.java index 9a2c74305..e1c060ee2 100644 --- a/api/src/main/java/net/luckperms/api/node/types/InheritanceNode.java +++ b/api/src/main/java/net/luckperms/api/node/types/InheritanceNode.java @@ -31,7 +31,6 @@ import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.ScopedNode; - import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -68,6 +67,7 @@ public interface InheritanceNode extends ScopedNode { * * @param group the group name * @return the builder + * @throws IllegalArgumentException if {@code group} is not a valid group name */ @NonNull Builder group(@NonNull String group); diff --git a/api/src/main/java/net/luckperms/api/node/types/MetaNode.java b/api/src/main/java/net/luckperms/api/node/types/MetaNode.java index 0def5b34f..da8f2b29b 100644 --- a/api/src/main/java/net/luckperms/api/node/types/MetaNode.java +++ b/api/src/main/java/net/luckperms/api/node/types/MetaNode.java @@ -30,7 +30,6 @@ import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.ScopedNode; - import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -72,6 +71,7 @@ public interface MetaNode extends ScopedNode { * @param key the meta key to set * @param value the meta value to set * @return the builder + * @throws IllegalArgumentException if {@code key} is empty */ static @NonNull Builder builder(@NonNull String key, @NonNull String value) { return builder().key(key).value(value); @@ -87,6 +87,7 @@ interface Builder extends NodeBuilder { * * @param key the meta key * @return the builder + * @throws IllegalArgumentException if {@code key} is empty */ @NonNull Builder key(@NonNull String key); diff --git a/api/src/main/java/net/luckperms/api/node/types/PermissionNode.java b/api/src/main/java/net/luckperms/api/node/types/PermissionNode.java index bcd699de7..f992c10ef 100644 --- a/api/src/main/java/net/luckperms/api/node/types/PermissionNode.java +++ b/api/src/main/java/net/luckperms/api/node/types/PermissionNode.java @@ -30,7 +30,6 @@ import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.ScopedNode; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.OptionalInt; @@ -89,6 +88,7 @@ public interface PermissionNode extends ScopedNode { * * @param permission the permission * @return the builder + * @throws IllegalArgumentException if {@code permission} is empty */ @NonNull Builder permission(@NonNull String permission); diff --git a/api/src/main/java/net/luckperms/api/node/types/PrefixNode.java b/api/src/main/java/net/luckperms/api/node/types/PrefixNode.java index 5035da6f6..cd74241aa 100644 --- a/api/src/main/java/net/luckperms/api/node/types/PrefixNode.java +++ b/api/src/main/java/net/luckperms/api/node/types/PrefixNode.java @@ -28,7 +28,6 @@ import net.luckperms.api.LuckPermsProvider; import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeType; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/node/types/RegexPermissionNode.java b/api/src/main/java/net/luckperms/api/node/types/RegexPermissionNode.java index f9c1a6899..9e7f364ff 100644 --- a/api/src/main/java/net/luckperms/api/node/types/RegexPermissionNode.java +++ b/api/src/main/java/net/luckperms/api/node/types/RegexPermissionNode.java @@ -30,7 +30,6 @@ import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.ScopedNode; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Optional; @@ -76,6 +75,7 @@ public interface RegexPermissionNode extends ScopedNode { * * @param pattern the pattern * @return the builder + * @throws IllegalArgumentException if {@code pattern} is empty */ @NonNull Builder pattern(@NonNull String pattern); diff --git a/api/src/main/java/net/luckperms/api/node/types/SuffixNode.java b/api/src/main/java/net/luckperms/api/node/types/SuffixNode.java index a358c0ae3..3c80e83ca 100644 --- a/api/src/main/java/net/luckperms/api/node/types/SuffixNode.java +++ b/api/src/main/java/net/luckperms/api/node/types/SuffixNode.java @@ -28,7 +28,6 @@ import net.luckperms.api.LuckPermsProvider; import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeType; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/node/types/WeightNode.java b/api/src/main/java/net/luckperms/api/node/types/WeightNode.java index ed1da6fef..f01d1aa9f 100644 --- a/api/src/main/java/net/luckperms/api/node/types/WeightNode.java +++ b/api/src/main/java/net/luckperms/api/node/types/WeightNode.java @@ -30,7 +30,6 @@ import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.ScopedNode; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/api/src/main/java/net/luckperms/api/platform/Health.java b/api/src/main/java/net/luckperms/api/platform/Health.java new file mode 100644 index 000000000..25ece4bee --- /dev/null +++ b/api/src/main/java/net/luckperms/api/platform/Health.java @@ -0,0 +1,51 @@ +/* + * 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 net.luckperms.api.platform; + +import java.util.Map; + +/** + * Represents the "health" status (healthcheck) of a LuckPerms implementation. + * + * @since 5.5 + */ +public interface Health { + + /** + * Gets if LuckPerms is healthy. + * + * @return if LuckPerms is healthy + */ + boolean isHealthy(); + + /** + * Gets extra metadata/details about the healthcheck result. + * + * @return details about the healthcheck status + */ + Map getDetails(); + +} diff --git a/api/src/main/java/net/luckperms/api/platform/Platform.java b/api/src/main/java/net/luckperms/api/platform/Platform.java index 9ed38beee..9bc2d8c32 100644 --- a/api/src/main/java/net/luckperms/api/platform/Platform.java +++ b/api/src/main/java/net/luckperms/api/platform/Platform.java @@ -75,7 +75,11 @@ enum Type { SPONGE("Sponge"), NUKKIT("Nukkit"), VELOCITY("Velocity"), - FABRIC("Fabric"); + FABRIC("Fabric"), + NEOFORGE("NeoForge"), + FORGE("Forge"), + STANDALONE("Standalone"), + HYTALE("Hytale"); private final String friendlyName; diff --git a/api/src/main/java/net/luckperms/api/platform/PlayerAdapter.java b/api/src/main/java/net/luckperms/api/platform/PlayerAdapter.java index 8ac5c6844..1f60dc7d1 100644 --- a/api/src/main/java/net/luckperms/api/platform/PlayerAdapter.java +++ b/api/src/main/java/net/luckperms/api/platform/PlayerAdapter.java @@ -33,7 +33,6 @@ import net.luckperms.api.model.user.User; import net.luckperms.api.model.user.UserManager; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.UUID; diff --git a/api/src/main/java/net/luckperms/api/query/OptionKey.java b/api/src/main/java/net/luckperms/api/query/OptionKey.java index 609a0db52..16f9b8baf 100644 --- a/api/src/main/java/net/luckperms/api/query/OptionKey.java +++ b/api/src/main/java/net/luckperms/api/query/OptionKey.java @@ -26,7 +26,6 @@ package net.luckperms.api.query; import net.luckperms.api.node.metadata.NodeMetadataKey; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.ApiStatus.NonExtendable; diff --git a/api/src/main/java/net/luckperms/api/query/QueryOptions.java b/api/src/main/java/net/luckperms/api/query/QueryOptions.java index 0d10ac2b3..0ff603e79 100644 --- a/api/src/main/java/net/luckperms/api/query/QueryOptions.java +++ b/api/src/main/java/net/luckperms/api/query/QueryOptions.java @@ -29,7 +29,6 @@ import net.luckperms.api.context.ContextSatisfyMode; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.ApiStatus.NonExtendable; diff --git a/api/src/main/java/net/luckperms/api/query/SimpleOptionKey.java b/api/src/main/java/net/luckperms/api/query/SimpleOptionKey.java index 1a5dccb67..6efc5e2ba 100644 --- a/api/src/main/java/net/luckperms/api/query/SimpleOptionKey.java +++ b/api/src/main/java/net/luckperms/api/query/SimpleOptionKey.java @@ -27,6 +27,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Locale; import java.util.Objects; final class SimpleOptionKey implements OptionKey { @@ -35,7 +36,7 @@ final class SimpleOptionKey implements OptionKey { private final int hashCode; SimpleOptionKey(String name, Class type) { - this.name = name.toLowerCase(); + this.name = name.toLowerCase(Locale.ROOT); this.type = type; this.hashCode = Objects.hash(this.name, this.type); // cache hashcode } diff --git a/api/src/main/java/net/luckperms/api/query/dataorder/DataQueryOrder.java b/api/src/main/java/net/luckperms/api/query/dataorder/DataQueryOrder.java index 86467c36b..c78af6d7d 100644 --- a/api/src/main/java/net/luckperms/api/query/dataorder/DataQueryOrder.java +++ b/api/src/main/java/net/luckperms/api/query/dataorder/DataQueryOrder.java @@ -27,7 +27,6 @@ import net.luckperms.api.model.data.DataType; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Arrays; diff --git a/api/src/main/java/net/luckperms/api/query/dataorder/DataQueryOrderFunction.java b/api/src/main/java/net/luckperms/api/query/dataorder/DataQueryOrderFunction.java index 02af36156..d7e081821 100644 --- a/api/src/main/java/net/luckperms/api/query/dataorder/DataQueryOrderFunction.java +++ b/api/src/main/java/net/luckperms/api/query/dataorder/DataQueryOrderFunction.java @@ -28,7 +28,6 @@ import net.luckperms.api.model.PermissionHolder; import net.luckperms.api.model.data.DataType; import net.luckperms.api.query.OptionKey; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Comparator; diff --git a/api/src/main/java/net/luckperms/api/query/dataorder/DataTypeFilter.java b/api/src/main/java/net/luckperms/api/query/dataorder/DataTypeFilter.java index 40d933495..f027212da 100644 --- a/api/src/main/java/net/luckperms/api/query/dataorder/DataTypeFilter.java +++ b/api/src/main/java/net/luckperms/api/query/dataorder/DataTypeFilter.java @@ -27,7 +27,6 @@ import net.luckperms.api.model.data.DataType; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Arrays; diff --git a/api/src/main/java/net/luckperms/api/query/dataorder/DataTypeFilterFunction.java b/api/src/main/java/net/luckperms/api/query/dataorder/DataTypeFilterFunction.java index 274385efa..0ad457547 100644 --- a/api/src/main/java/net/luckperms/api/query/dataorder/DataTypeFilterFunction.java +++ b/api/src/main/java/net/luckperms/api/query/dataorder/DataTypeFilterFunction.java @@ -28,7 +28,6 @@ import net.luckperms.api.model.PermissionHolder; import net.luckperms.api.model.data.DataType; import net.luckperms.api.query.OptionKey; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; @@ -49,7 +48,7 @@ public interface DataTypeFilterFunction { /** * Creates a {@link DataTypeFilterFunction} that always returns the given - * {@code predicate}. + * {@code predicate} (commonly one of the values in {@link DataTypeFilter}). * * @param predicate the predicate * @return the data type filter function diff --git a/api/src/main/java/net/luckperms/api/query/meta/MetaValueSelector.java b/api/src/main/java/net/luckperms/api/query/meta/MetaValueSelector.java index ceb2b46a2..424126387 100644 --- a/api/src/main/java/net/luckperms/api/query/meta/MetaValueSelector.java +++ b/api/src/main/java/net/luckperms/api/query/meta/MetaValueSelector.java @@ -25,9 +25,9 @@ package net.luckperms.api.query.meta; +import net.luckperms.api.cacheddata.Result; import net.luckperms.api.node.types.MetaNode; import net.luckperms.api.query.OptionKey; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; @@ -54,6 +54,6 @@ public interface MetaValueSelector { * @param values the values, in the order in which they were accumulated. * @return the selected value */ - @NonNull String selectValue(@NonNull String key, @NonNull List values); + @NonNull Result selectValue(@NonNull String key, @NonNull List> values); } diff --git a/api/src/main/java/net/luckperms/api/track/DemotionResult.java b/api/src/main/java/net/luckperms/api/track/DemotionResult.java index f43720e8c..214f51c64 100644 --- a/api/src/main/java/net/luckperms/api/track/DemotionResult.java +++ b/api/src/main/java/net/luckperms/api/track/DemotionResult.java @@ -27,7 +27,6 @@ import net.luckperms.api.model.user.User; import net.luckperms.api.util.Result; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Optional; diff --git a/api/src/main/java/net/luckperms/api/track/PromotionResult.java b/api/src/main/java/net/luckperms/api/track/PromotionResult.java index fdd713b68..41ae090d5 100644 --- a/api/src/main/java/net/luckperms/api/track/PromotionResult.java +++ b/api/src/main/java/net/luckperms/api/track/PromotionResult.java @@ -27,7 +27,6 @@ import net.luckperms.api.model.user.User; import net.luckperms.api.util.Result; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Optional; diff --git a/api/src/main/java/net/luckperms/api/track/Track.java b/api/src/main/java/net/luckperms/api/track/Track.java index 4ba433de6..83538c502 100644 --- a/api/src/main/java/net/luckperms/api/track/Track.java +++ b/api/src/main/java/net/luckperms/api/track/Track.java @@ -29,7 +29,6 @@ import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.group.Group; import net.luckperms.api.model.user.User; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.Unmodifiable; diff --git a/api/src/main/java/net/luckperms/api/track/TrackManager.java b/api/src/main/java/net/luckperms/api/track/TrackManager.java index f1719a673..c5c22ae4e 100644 --- a/api/src/main/java/net/luckperms/api/track/TrackManager.java +++ b/api/src/main/java/net/luckperms/api/track/TrackManager.java @@ -96,6 +96,27 @@ public interface TrackManager { */ @NonNull CompletableFuture deleteTrack(@NonNull Track track); + /** + * Loads (or creates) a track from the plugin's storage provider, applies the given {@code action}, + * then saves the track's data back to storage. + * + *

    This method effectively calls {@link #createAndLoadTrack(String)}, followed by the + * {@code action}, then {@link #saveTrack(Track)}, and returns an encapsulation of the whole + * process as a {@link CompletableFuture}.

    + * + * @param name the name of the track + * @param action the action to apply to the track + * @return a future to encapsulate the operation + * @since 5.5 + */ + default @NonNull CompletableFuture modifyTrack(@NonNull String name, @NonNull Consumer action) { + /* This default method is overridden in the implementation, and is just here + to demonstrate what this method does in the API sources. */ + return createAndLoadTrack(name) + .thenApplyAsync(track -> { action.accept(track); return track; }) + .thenCompose(this::saveTrack); + } + /** * Loads all tracks into memory. * diff --git a/api/src/main/java/net/luckperms/api/util/Page.java b/api/src/main/java/net/luckperms/api/util/Page.java new file mode 100644 index 000000000..3ecf589c4 --- /dev/null +++ b/api/src/main/java/net/luckperms/api/util/Page.java @@ -0,0 +1,53 @@ +/* + * 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 net.luckperms.api.util; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.List; + +/** + * Represents a page of entries. + * + * @since 5.5 + */ +public interface Page { + + /** + * Gets the entries on this page. + * + * @return the entries + */ + @NonNull List entries(); + + /** + * Gets the total/overall number of entries (not just the number of entries on this page). + * + * @return the total number of entries + */ + int overallSize(); + +} diff --git a/build.gradle b/build.gradle index ec7ec634f..89b8f5e15 100644 --- a/build.gradle +++ b/build.gradle @@ -1,28 +1,32 @@ -buildscript { - repositories { - maven { url 'https://plugins.gradle.org/m2' } - } - - dependencies { - classpath 'gradle.plugin.net.minecrell:licenser:0.4.1' - } +plugins { + alias(libs.plugins.licenser) apply false + alias(libs.plugins.loom) apply false } -defaultTasks 'licenseFormat', 'build' +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent subprojects { apply plugin: 'java' - apply plugin: 'maven' - apply plugin: 'net.minecrell.licenser' + apply plugin: 'maven-publish' + apply plugin: 'dev.yumi.gradle.licenser' group = 'me.lucko.luckperms' - version = '5.3-SNAPSHOT' - - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + version = '5.5-SNAPSHOT' - tasks.withType(JavaCompile) { + tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' + options.release = 11 + } + + tasks.withType(Test).configureEach { + testLogging { + events = [TestLogEvent.PASSED, TestLogEvent.FAILED, TestLogEvent.SKIPPED] + exceptionFormat = TestExceptionFormat.FULL + showExceptions = true + showCauses = true + showStackTraces = true + } } jar { @@ -30,35 +34,37 @@ subprojects { } def determinePatchVersion = { - // get the name of the last tag - def tagInfo = new ByteArrayOutputStream() - exec { + def tagInfo = providers.exec { commandLine 'git', 'describe', '--tags' - standardOutput = tagInfo - } - tagInfo = tagInfo.toString() + }.standardOutput.asText.get().trim() - if (!tagInfo.contains('-')) { - return 0 - } - return tagInfo.split("-")[1] + return tagInfo.contains('-') ? tagInfo.split('-')[1] : 0 } project.ext.majorVersion = '5' - project.ext.minorVersion = '3' + project.ext.minorVersion = '5' project.ext.patchVersion = determinePatchVersion() project.ext.apiVersion = project.ext.majorVersion + '.' + project.ext.minorVersion project.ext.fullVersion = project.ext.apiVersion + '.' + project.ext.patchVersion license { - header = rootProject.file('HEADER.txt') - include '**/*.java' - newLine = true + rule(rootProject.file("HEADER.txt")) + exclude '*.xml' } repositories { + // Fix issue with lwjgl-freetype not being found on macOS / ForgeGradle issue + // + // Could not resolve all files for configuration ':_compileJava_1'. + // Could not find lwjgl-freetype-3.3.3-natives-macos-patch.jar (org.lwjgl:lwjgl-freetype:3.3.3). + maven { + url "https://libraries.minecraft.net" + content { + includeModule("org.lwjgl", "lwjgl-freetype") + } + } mavenCentral() - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://repo.lucko.me/' } + maven { url 'https://libraries.minecraft.net/' } } } diff --git a/bukkit-legacy/build.gradle b/bukkit-legacy/build.gradle index bf3c3d79a..2cbdccf4a 100644 --- a/bukkit-legacy/build.gradle +++ b/bukkit-legacy/build.gradle @@ -1,15 +1,15 @@ plugins { - id 'com.github.johnrengelman.shadow' version '2.0.4' + alias(libs.plugins.shadow) } dependencies { - compile project(':bukkit') - compile 'com.google.code.gson:gson:2.7' - compile 'com.google.guava:guava:19.0' + implementation project(':bukkit') + implementation 'com.google.code.gson:gson:2.7' + implementation 'com.google.guava:guava:19.0' } shadowJar { - archiveName = 'luckperms-bukkitlegacy.jarinjar' + archiveFileName = 'luckperms-bukkitlegacy.jarinjar' dependencies { include(dependency('me.lucko.luckperms:.*')) @@ -34,6 +34,7 @@ shadowJar { relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' @@ -41,4 +42,4 @@ shadowJar { artifacts { archives shadowJar -} \ No newline at end of file +} diff --git a/bukkit-legacy/loader/build.gradle b/bukkit-legacy/loader/build.gradle index f0dda1442..92b9a4e1d 100644 --- a/bukkit-legacy/loader/build.gradle +++ b/bukkit-legacy/loader/build.gradle @@ -1,27 +1,26 @@ plugins { - id 'com.github.johnrengelman.shadow' + alias(libs.plugins.shadow) } repositories { - maven { url 'https://papermc.io/repo/repository/maven-public/' } + maven { url 'https://repo.papermc.io/repository/maven-public/' } } dependencies { compileOnly 'com.destroystokyo.paper:paper-api:1.15.2-R0.1-SNAPSHOT' - compile project(':api') - compile project(':common:loader-utils') + implementation project(':api') + implementation project(':common:loader-utils') } processResources { - from(sourceSets.main.resources.srcDirs) { + filesMatching('plugin.yml') { expand 'pluginVersion': project.ext.fullVersion - include 'plugin.yml' } } shadowJar { - archiveName = "LuckPerms-Bukkit-Legacy-${project.ext.fullVersion}.jar" + archiveFileName = "LuckPerms-Bukkit-Legacy-${project.ext.fullVersion}.jar" from { project(':bukkit-legacy').tasks.shadowJar.archiveFile @@ -30,4 +29,4 @@ shadowJar { artifacts { archives shadowJar -} \ No newline at end of file +} diff --git a/bukkit-legacy/loader/src/main/java/me/lucko/luckperms/bukkit/loader/BukkitLegacyLoaderPlugin.java b/bukkit-legacy/loader/src/main/java/me/lucko/luckperms/bukkit/loader/BukkitLegacyLoaderPlugin.java index aad2d7d24..b843f1f73 100644 --- a/bukkit-legacy/loader/src/main/java/me/lucko/luckperms/bukkit/loader/BukkitLegacyLoaderPlugin.java +++ b/bukkit-legacy/loader/src/main/java/me/lucko/luckperms/bukkit/loader/BukkitLegacyLoaderPlugin.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.loader.JarInJarClassLoader; import me.lucko.luckperms.common.loader.LoaderBootstrap; - import org.bukkit.plugin.java.JavaPlugin; public class BukkitLegacyLoaderPlugin extends JavaPlugin { diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 1abccbb1c..1c1ea2822 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -1,31 +1,34 @@ plugins { - id 'com.github.johnrengelman.shadow' version '2.0.4' + alias(libs.plugins.shadow) } repositories { - maven { url 'https://papermc.io/repo/repository/maven-public/' } - maven { url 'https://libraries.minecraft.net/' } + maven { url 'https://repo.papermc.io/repository/maven-public/' } +} + +java { + disableAutoTargetJvm() } dependencies { - compile project(':common') + implementation project(':common') compileOnly project(':common:loader-utils') - compileOnly 'com.destroystokyo.paper:paper-api:1.15.2-R0.1-SNAPSHOT' - compileOnly('me.lucko:adventure-platform-bukkit:4.7.0') { + compileOnly 'dev.folia:folia-api:26.1.2.build.8-stable' + compileOnly('net.kyori:adventure-platform-bukkit:4.4.0') { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') exclude(module: 'adventure-nbt') } - compileOnly 'me.lucko:commodore:1.9' - compileOnly('net.milkbowl.vault:VaultAPI:1.6') { + compileOnly 'me.lucko:commodore:2.0' + compileOnly('net.milkbowl.vault:VaultAPI:1.7') { exclude(module: 'bukkit') } compileOnly 'lilypad.client.connect:api:0.0.1-SNAPSHOT' } shadowJar { - archiveName = 'luckperms-bukkit.jarinjar' + archiveFileName = 'luckperms-bukkit.jarinjar' dependencies { include(dependency('me.lucko.luckperms:.*')) @@ -45,6 +48,7 @@ shadowJar { relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' @@ -52,4 +56,4 @@ shadowJar { artifacts { archives shadowJar -} \ No newline at end of file +} diff --git a/bukkit/loader/build.gradle b/bukkit/loader/build.gradle index f6c56513b..eb442a2b4 100644 --- a/bukkit/loader/build.gradle +++ b/bukkit/loader/build.gradle @@ -1,33 +1,36 @@ plugins { - id 'com.github.johnrengelman.shadow' + alias(libs.plugins.shadow) } repositories { - maven { url 'https://papermc.io/repo/repository/maven-public/' } + maven { url 'https://repo.papermc.io/repository/maven-public/' } } dependencies { compileOnly 'com.destroystokyo.paper:paper-api:1.15.2-R0.1-SNAPSHOT' - compile project(':api') - compile project(':common:loader-utils') + implementation project(':api') + implementation project(':common:loader-utils') } processResources { - from(sourceSets.main.resources.srcDirs) { + filesMatching('plugin.yml') { expand 'pluginVersion': project.ext.fullVersion - include 'plugin.yml' } } shadowJar { - archiveName = "LuckPerms-Bukkit-${project.ext.fullVersion}.jar" + archiveFileName = "LuckPerms-Bukkit-${project.ext.fullVersion}.jar" from { project(':bukkit').tasks.shadowJar.archiveFile } + + manifest { + attributes(["paperweight-mappings-namespace": "mojang"]) + } } artifacts { archives shadowJar -} \ No newline at end of file +} diff --git a/bukkit/loader/src/main/java/me/lucko/luckperms/bukkit/loader/BukkitLoaderPlugin.java b/bukkit/loader/src/main/java/me/lucko/luckperms/bukkit/loader/BukkitLoaderPlugin.java index 4f149edc4..1260b7f8c 100644 --- a/bukkit/loader/src/main/java/me/lucko/luckperms/bukkit/loader/BukkitLoaderPlugin.java +++ b/bukkit/loader/src/main/java/me/lucko/luckperms/bukkit/loader/BukkitLoaderPlugin.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.loader.JarInJarClassLoader; import me.lucko.luckperms.common.loader.LoaderBootstrap; - import org.bukkit.plugin.java.JavaPlugin; public class BukkitLoaderPlugin extends JavaPlugin { diff --git a/bukkit/loader/src/main/resources/plugin.yml b/bukkit/loader/src/main/resources/plugin.yml index 708025aee..45db9e648 100644 --- a/bukkit/loader/src/main/resources/plugin.yml +++ b/bukkit/loader/src/main/resources/plugin.yml @@ -10,6 +10,7 @@ load: STARTUP # remapping when the plugin is loaded. Note that despite what this setting might otherwise imply, # LP is still compatible with pre-1.13 releases. api-version: 1.13 +folia-supported: true # Load LuckPerms before Vault. This means that all plugins that (soft-)depend # on Vault depend on LuckPerms too. diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitAsyncCommandExecutor.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitAsyncCommandExecutor.java index 5db5186d4..f3a15ba51 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitAsyncCommandExecutor.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitAsyncCommandExecutor.java @@ -26,11 +26,9 @@ package me.lucko.luckperms.bukkit; import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent; - import me.lucko.luckperms.bukkit.util.CommandMapUtil; import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; import me.lucko.luckperms.common.sender.Sender; - import org.bukkit.command.Command; import org.bukkit.command.PluginCommand; import org.bukkit.event.EventHandler; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitCommandExecutor.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitCommandExecutor.java index 70d36f3a4..24fd39ab2 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitCommandExecutor.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitCommandExecutor.java @@ -30,7 +30,6 @@ import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.sender.Sender; - import org.bukkit.Server; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitConfigAdapter.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitConfigAdapter.java index 1f3107253..65b2bb467 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitConfigAdapter.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitConfigAdapter.java @@ -27,16 +27,13 @@ import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; public class BukkitConfigAdapter implements ConfigurationAdapter { private final LuckPermsPlugin plugin; @@ -75,17 +72,6 @@ public List getStringList(String path, List def) { return this.configuration.isSet(path) ? list : def; } - @Override - public List getKeys(String path, List def) { - ConfigurationSection section = this.configuration.getConfigurationSection(path); - if (section == null) { - return def; - } - - Set keys = section.getKeys(false); - return keys == null ? def : new ArrayList<>(keys); - } - @Override public Map getStringMap(String path, Map def) { Map map = new HashMap<>(); diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitEventBus.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitEventBus.java index cc22fe414..469730103 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitEventBus.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitEventBus.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.event.AbstractEventBus; - import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.PluginDisableEvent; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitSchedulerAdapter.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitSchedulerAdapter.java index 93095e3bf..42800b5ee 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitSchedulerAdapter.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitSchedulerAdapter.java @@ -25,21 +25,32 @@ package me.lucko.luckperms.bukkit; -import me.lucko.luckperms.common.plugin.scheduler.AbstractJavaScheduler; +import me.lucko.luckperms.common.plugin.scheduler.JavaSchedulerAdapter; import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.common.sender.Sender; +import org.bukkit.command.CommandSender; import java.util.concurrent.Executor; -public class BukkitSchedulerAdapter extends AbstractJavaScheduler implements SchedulerAdapter { - private final Executor sync; +public class BukkitSchedulerAdapter extends JavaSchedulerAdapter implements SchedulerAdapter { + private final Executor syncExecutor; public BukkitSchedulerAdapter(LPBukkitBootstrap bootstrap) { - this.sync = r -> bootstrap.getServer().getScheduler().scheduleSyncDelayedTask(bootstrap.getLoader(), r); + super(bootstrap); + this.syncExecutor = r -> bootstrap.getServer().getScheduler().scheduleSyncDelayedTask(bootstrap.getLoader(), r); + } + + public void executeSync(Runnable task) { + this.syncExecutor.execute(task); + } + + public void executeSync(CommandSender ctx, Runnable task) { + this.syncExecutor.execute(task); } @Override - public Executor sync() { - return this.sync; + public void executeSync(Sender ctx, Runnable task) { + this.syncExecutor.execute(task); } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitSenderFactory.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitSenderFactory.java index df38b583a..d043cd8d5 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitSenderFactory.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/BukkitSenderFactory.java @@ -25,26 +25,34 @@ package me.lucko.luckperms.bukkit; +import me.lucko.luckperms.bukkit.util.PaperAdventureBridge; +import me.lucko.luckperms.bukkit.util.PlayerLocaleUtil; +import me.lucko.luckperms.common.locale.TranslationManager; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.sender.SenderFactory; - import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.text.Component; import net.luckperms.api.util.Tristate; - import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.command.RemoteConsoleCommandSender; import org.bukkit.entity.Player; +import java.util.Locale; import java.util.UUID; -public class BukkitSenderFactory extends SenderFactory { - private final BukkitAudiences audiences; +public abstract class BukkitSenderFactory extends SenderFactory { + + public static BukkitSenderFactory create(LPBukkitPlugin plugin) { + PaperAdventureBridge bridge = PaperAdventureBridge.INSTANCE; + return bridge != null + ? new PaperBridgeBukkitSenderFactory(plugin, bridge) + : new AdventurePlatformBukkitSenderFactory(plugin); + + } public BukkitSenderFactory(LPBukkitPlugin plugin) { super(plugin); - this.audiences = BukkitAudiences.create(plugin.getLoader()); } @Override @@ -67,12 +75,14 @@ protected UUID getUniqueId(CommandSender sender) { protected void sendMessage(CommandSender sender, Component message) { // we can safely send async for players and the console - otherwise, send it sync if (sender instanceof Player || sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender) { - this.audiences.sender(sender).sendMessage(message); + sendMessage0(sender, message); } else { - getPlugin().getBootstrap().getScheduler().executeSync(() -> this.audiences.sender(sender).sendMessage(message)); + getPlugin().getBootstrap().getScheduler().executeSync(sender, () -> sendMessage0(sender, message)); } } + protected abstract void sendMessage0(CommandSender sender, Component message); + @Override protected Tristate getPermissionValue(CommandSender sender, String node) { if (sender.hasPermission(node)) { @@ -99,9 +109,48 @@ protected boolean isConsole(CommandSender sender) { return sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender; } - @Override - public void close() { - super.close(); - this.audiences.close(); + /** + * Sender factory that uses native Paper adventure support to send messages. + */ + private static class PaperBridgeBukkitSenderFactory extends BukkitSenderFactory { + private final PaperAdventureBridge bridge; + + public PaperBridgeBukkitSenderFactory(LPBukkitPlugin plugin, PaperAdventureBridge bridge) { + super(plugin); + this.bridge = bridge; + } + + @Override + protected void sendMessage0(CommandSender sender, Component message) { + Locale locale = null; + if (sender instanceof Player) { + locale = PlayerLocaleUtil.getLocale((Player) sender); + } + Component rendered = TranslationManager.render(message, locale); + this.bridge.sendMessage(sender, rendered); + } + } + + /** + * Sender factory that uses the adventure-platform library to send messages. + */ + private static class AdventurePlatformBukkitSenderFactory extends BukkitSenderFactory { + private final BukkitAudiences audiences; + + public AdventurePlatformBukkitSenderFactory(LPBukkitPlugin plugin) { + super(plugin); + this.audiences = BukkitAudiences.create(plugin.getLoader()); + } + + @Override + protected void sendMessage0(CommandSender sender, Component message) { + this.audiences.sender(sender).sendMessage(message); + } + + @Override + public void close() { + super.close(); + this.audiences.close(); + } } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/FoliaSchedulerAdapter.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/FoliaSchedulerAdapter.java new file mode 100644 index 000000000..69e47c6dc --- /dev/null +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/FoliaSchedulerAdapter.java @@ -0,0 +1,86 @@ +/* + * 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.bukkit; + +import io.papermc.paper.threadedregions.scheduler.RegionScheduler; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.common.sender.AbstractSender; +import me.lucko.luckperms.common.sender.Sender; +import org.bukkit.Server; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.ProxiedCommandSender; +import org.bukkit.command.RemoteConsoleCommandSender; +import org.bukkit.entity.Entity; +import org.bukkit.plugin.java.JavaPlugin; + +public class FoliaSchedulerAdapter extends BukkitSchedulerAdapter implements SchedulerAdapter { + private final JavaPlugin loader; + private final Server server; + + public FoliaSchedulerAdapter(LPBukkitBootstrap bootstrap) { + super(bootstrap); + this.loader = bootstrap.getLoader(); + this.server = bootstrap.getServer(); + } + + @Override + public void executeSync(Runnable task) { + this.server.getGlobalRegionScheduler().execute(this.loader, task); + } + + @Override + public void executeSync(Sender ctx, Runnable task) { + executeSync(unwrapSender(ctx), task); + } + + @Override + public void executeSync(CommandSender ctx, Runnable task) { + if (ctx instanceof Entity) { + ((Entity) ctx).getScheduler().execute(this.loader, task, null, 0); + } else if (ctx instanceof BlockCommandSender) { + RegionScheduler scheduler = this.server.getRegionScheduler(); + scheduler.execute(this.loader, ((BlockCommandSender) ctx).getBlock().getLocation(), task); + } else if (ctx instanceof ConsoleCommandSender || ctx instanceof RemoteConsoleCommandSender) { + this.server.getGlobalRegionScheduler().execute(this.loader, task); + } else if (ctx instanceof ProxiedCommandSender) { + executeSync(((ProxiedCommandSender) ctx).getCallee(), task); + } else { + throw new IllegalArgumentException("Unknown command sender type: " + ctx.getClass().getName()); + } + } + + @SuppressWarnings("unchecked") + private static CommandSender unwrapSender(Sender sender) { + if (sender instanceof AbstractSender) { + return ((AbstractSender) sender).getSender(); + } else { + throw new IllegalArgumentException("unknown sender type: " + sender.getClass()); + } + } + +} diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitBootstrap.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitBootstrap.java index fd9333f76..c57427cc2 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitBootstrap.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitBootstrap.java @@ -27,14 +27,13 @@ import me.lucko.luckperms.bukkit.util.NullSafeConsoleCommandSender; import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; import me.lucko.luckperms.common.plugin.logging.JavaPluginLogger; import me.lucko.luckperms.common.plugin.logging.PluginLogger; - import net.luckperms.api.platform.Platform; - import org.bukkit.OfflinePlayer; import org.bukkit.Server; import org.bukkit.command.ConsoleCommandSender; @@ -56,7 +55,7 @@ /** * Bootstrap plugin for LuckPerms running on Bukkit. */ -public class LPBukkitBootstrap implements LuckPermsBootstrap, LoaderBootstrap { +public class LPBukkitBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { private final JavaPlugin loader; /** @@ -103,7 +102,9 @@ public LPBukkitBootstrap(JavaPlugin loader) { this.loader = loader; this.logger = new JavaPluginLogger(loader.getLogger()); - this.schedulerAdapter = new BukkitSchedulerAdapter(this); + this.schedulerAdapter = isFolia() + ? new FoliaSchedulerAdapter(this) + : new BukkitSchedulerAdapter(this); this.classPathAppender = new JarInJarClassPathAppender(getClass().getClassLoader()); this.console = new NullSafeConsoleCommandSender(getServer()); this.plugin = new LPBukkitPlugin(this); @@ -111,6 +112,7 @@ public LPBukkitBootstrap(JavaPlugin loader) { // provide adapters + @Override public JavaPlugin getLoader() { return this.loader; } @@ -175,7 +177,7 @@ public void onEnable() { this.plugin.enable(); // schedule a task to update the 'serverStarting' flag - getServer().getScheduler().runTask(this.loader, () -> this.serverStarting = false); + this.schedulerAdapter.executeSync(() -> this.serverStarting = false); } finally { this.enableLatch.countDown(); } @@ -307,8 +309,17 @@ private static boolean checkIncompatibleVersion() { try { Class.forName("com.google.gson.JsonElement"); return false; - } catch (ClassNotFoundException e) { + } catch (Exception e) { return true; } } + + private static boolean isFolia() { + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } } 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 44b322766..295341ab4 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/LPBukkitPlugin.java @@ -60,12 +60,8 @@ import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.tasks.CacheHousekeepingTask; -import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; - import net.luckperms.api.LuckPerms; import net.luckperms.api.query.QueryOptions; - import org.bukkit.OfflinePlayer; import org.bukkit.command.PluginCommand; import org.bukkit.entity.Player; @@ -79,7 +75,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.stream.Stream; /** @@ -115,7 +110,7 @@ public JavaPlugin getLoader() { @Override protected void setupSenderFactory() { - this.senderFactory = new BukkitSenderFactory(this); + this.senderFactory = BukkitSenderFactory.create(this); } @Override @@ -211,7 +206,7 @@ protected void setupPlatformHooks() { // schedule another injection after all plugins have loaded // the entire pluginmanager instance is replaced by some plugins :( - this.bootstrap.getServer().getScheduler().runTaskLaterAsynchronously(this.bootstrap.getLoader(), injector, 1); + this.bootstrap.getScheduler().executeSync(injector); } /* @@ -222,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 */ @@ -259,12 +254,6 @@ protected void registerApiOnPlatform(LuckPerms api) { this.bootstrap.getServer().getServicesManager().register(LuckPerms.class, api, this.bootstrap.getLoader(), ServicePriority.Normal); } - @Override - protected void registerHousekeepingTasks() { - this.bootstrap.getScheduler().asyncRepeating(new ExpireTemporaryTask(this), 3, TimeUnit.SECONDS); - this.bootstrap.getScheduler().asyncRepeating(new CacheHousekeepingTask(this), 2, TimeUnit.MINUTES); - } - @Override protected void performFinalSetup() { // register permissions @@ -279,7 +268,7 @@ protected void performFinalSetup() { // remove all operators on startup if they're disabled if (!getConfiguration().get(ConfigKeys.OPS_ENABLED)) { - this.bootstrap.getServer().getScheduler().runTaskAsynchronously(this.bootstrap.getLoader(), () -> { + this.bootstrap.getScheduler().executeSync(() -> { for (OfflinePlayer player : this.bootstrap.getServer().getOperators()) { player.setOp(false); } @@ -302,7 +291,7 @@ protected void performFinalSetup() { try { User user = this.connectionListener.loadUser(player.getUniqueId(), player.getName()); if (user != null) { - this.bootstrap.getScheduler().executeSync(() -> { + this.bootstrap.getScheduler().executeSync(player, () -> { try { LuckPermsPermissible lpPermissible = new LuckPermsPermissible(player, user, this); PermissibleInjector.inject(player, lpPermissible, getLogger()); @@ -358,7 +347,7 @@ private static boolean classExists(String className) { try { Class.forName(className); return true; - } catch (ClassNotFoundException e) { + } catch (Exception e) { return false; } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/brigadier/LuckPermsBrigadier.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/brigadier/LuckPermsBrigadier.java index 8c8eaf348..11a01bf6c 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/brigadier/LuckPermsBrigadier.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/brigadier/LuckPermsBrigadier.java @@ -26,13 +26,11 @@ package me.lucko.luckperms.bukkit.brigadier; import com.mojang.brigadier.tree.LiteralCommandNode; - import me.lucko.commodore.Commodore; import me.lucko.commodore.CommodoreProvider; -import me.lucko.commodore.file.CommodoreFileFormat; +import me.lucko.commodore.file.CommodoreFileReader; import me.lucko.luckperms.bukkit.LPBukkitPlugin; import me.lucko.luckperms.common.sender.Sender; - import org.bukkit.command.Command; import java.io.InputStream; @@ -50,7 +48,7 @@ public static void register(LPBukkitPlugin plugin, Command pluginCommand) throws throw new Exception("Brigadier command data missing from jar"); } - LiteralCommandNode commandNode = CommodoreFileFormat.parse(is); + LiteralCommandNode commandNode = CommodoreFileReader.INSTANCE.parse(is); commodore.register(pluginCommand, commandNode, player -> { Sender playerAsSender = plugin.getSenderFactory().wrap(player); return plugin.getCommandManager().hasPermissionForAny(playerAsSender); diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/BukkitCalculatorFactory.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/BukkitCalculatorFactory.java index 5233544dd..de40d3c1a 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/BukkitCalculatorFactory.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/BukkitCalculatorFactory.java @@ -30,6 +30,7 @@ import me.lucko.luckperms.common.cacheddata.CacheMetadata; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorMonitored; import me.lucko.luckperms.common.calculator.processor.DirectProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import me.lucko.luckperms.common.calculator.processor.RegexProcessor; @@ -37,11 +38,12 @@ import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.model.HolderType; - +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class BukkitCalculatorFactory implements CalculatorFactory { private final LPBukkitPlugin plugin; @@ -51,37 +53,38 @@ public BukkitCalculatorFactory(LPBukkitPlugin plugin) { } @Override - public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { - List processors = new ArrayList<>(7); + public PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata) { + List processors = new ArrayList<>(8); - processors.add(new DirectProcessor()); + processors.add(new DirectProcessor(sourceMap)); if (this.plugin.getConfiguration().get(ConfigKeys.APPLY_BUKKIT_CHILD_PERMISSIONS)) { - processors.add(new ChildProcessor(this.plugin)); + processors.add(new ChildProcessor(this.plugin, sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { - processors.add(new RegexProcessor()); + processors.add(new RegexProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { - processors.add(new WildcardProcessor()); + processors.add(new WildcardProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { - processors.add(new SpongeWildcardProcessor()); + processors.add(new SpongeWildcardProcessor(sourceMap)); } boolean op = queryOptions.option(BukkitContextManager.OP_OPTION).orElse(false); if (metadata.getHolderType() == HolderType.USER && this.plugin.getConfiguration().get(ConfigKeys.APPLY_BUKKIT_DEFAULT_PERMISSIONS)) { boolean overrideWildcards = this.plugin.getConfiguration().get(ConfigKeys.APPLY_DEFAULT_NEGATIONS_BEFORE_WILDCARDS); - processors.add(new DefaultsProcessor(this.plugin, overrideWildcards, op)); + processors.add(new DefaultPermissionMapProcessor(this.plugin, op)); + processors.add(new PermissionMapProcessor(this.plugin, overrideWildcards, op)); } if (op) { processors.add(OpProcessor.INSTANCE); } - return new PermissionCalculator(this.plugin, metadata, processors); + return new PermissionCalculatorMonitored(this.plugin, metadata, processors); } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/ChildProcessor.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/ChildProcessor.java index 8ee84610b..a942a512f 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/ChildProcessor.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/ChildProcessor.java @@ -26,13 +26,13 @@ package me.lucko.luckperms.bukkit.calculator; import me.lucko.luckperms.bukkit.LPBukkitPlugin; +import me.lucko.luckperms.bukkit.inject.server.LuckPermsPermissionMap; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; - +import net.luckperms.api.node.Node; import net.luckperms.api.util.Tristate; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -44,11 +44,19 @@ public class ChildProcessor extends AbstractPermissionProcessor implements Permi private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(ChildProcessor.class); private final LPBukkitPlugin plugin; + private final Map sourceMap; + private final AtomicBoolean needsRefresh = new AtomicBoolean(false); - private Map childPermissions = Collections.emptyMap(); + private Map childPermissions; - public ChildProcessor(LPBukkitPlugin plugin) { + public ChildProcessor(LPBukkitPlugin plugin, Map sourceMap) { this.plugin = plugin; + this.sourceMap = sourceMap; + refresh(); + } + + private void refresh() { + this.childPermissions = processChildPermissions(this.sourceMap, this.plugin.getPermissionMap()); } @Override @@ -60,20 +68,18 @@ public TristateResult hasPermission(String permission) { } @Override - public void refresh() { + public void invalidate() { + this.needsRefresh.set(true); + } + + private static Map processChildPermissions(Map sourceMap, LuckPermsPermissionMap permissionMap) { Map childPermissions = new HashMap<>(); - this.sourceMap.forEach((key, value) -> { - Map children = this.plugin.getPermissionMap().getChildPermissions(key, value); + sourceMap.forEach((key, node) -> { + Map children = permissionMap.getChildPermissions(key, node.getValue()); children.forEach((childKey, childValue) -> { - childPermissions.put(childKey, RESULT_FACTORY.result(Tristate.of(childValue), "parent: " + key)); + childPermissions.put(childKey, RESULT_FACTORY.resultWithOverride(node, Tristate.of(childValue))); }); }); - this.childPermissions = childPermissions; - this.needsRefresh.set(false); - } - - @Override - public void invalidate() { - this.needsRefresh.set(true); + return childPermissions; } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/DefaultPermissionMapProcessor.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/DefaultPermissionMapProcessor.java new file mode 100644 index 000000000..8f12d751c --- /dev/null +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/DefaultPermissionMapProcessor.java @@ -0,0 +1,57 @@ +/* + * 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.bukkit.calculator; + +import me.lucko.luckperms.bukkit.LPBukkitPlugin; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import net.luckperms.api.util.Tristate; + +/** + * Permission Processor for Bukkits "default" permission system. + */ +public class DefaultPermissionMapProcessor extends AbstractPermissionProcessor implements PermissionProcessor { + private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(DefaultPermissionMapProcessor.class); + + private final LPBukkitPlugin plugin; + private final boolean isOp; + + public DefaultPermissionMapProcessor(LPBukkitPlugin plugin, boolean isOp) { + this.plugin = plugin; + this.isOp = isOp; + } + + @Override + public TristateResult hasPermission(String permission) { + Tristate t = this.plugin.getDefaultPermissionMap().lookupDefaultPermission(permission, this.isOp); + if (t != Tristate.UNDEFINED) { + return RESULT_FACTORY.result(t); + } + + return TristateResult.UNDEFINED; + } +} diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/DefaultsProcessor.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/DefaultsProcessor.java deleted file mode 100644 index 4f0a6a846..000000000 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/DefaultsProcessor.java +++ /dev/null @@ -1,90 +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.bukkit.calculator; - -import me.lucko.luckperms.bukkit.LPBukkitPlugin; -import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; -import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; -import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; - -import net.luckperms.api.util.Tristate; - -import org.bukkit.permissions.Permission; -import org.bukkit.permissions.PermissionDefault; - -/** - * Permission Processor for Bukkits "default" permission system. - */ -public class DefaultsProcessor implements PermissionProcessor { - private static final TristateResult.Factory DEFAULT_PERMISSION_MAP_RESULT_FACTORY = new TristateResult.Factory(DefaultsProcessor.class, "default permission map"); - private static final TristateResult.Factory PERMISSION_MAP_RESULT_FACTORY = new TristateResult.Factory(DefaultsProcessor.class, "permission map"); - - private final LPBukkitPlugin plugin; - private final boolean overrideWildcards; - private final boolean isOp; - - public DefaultsProcessor(LPBukkitPlugin plugin, boolean overrideWildcards, boolean isOp) { - this.plugin = plugin; - this.overrideWildcards = overrideWildcards; - this.isOp = isOp; - } - - private boolean canOverrideWildcard(TristateResult prev) { - return this.overrideWildcards && - (prev.processorClass() == WildcardProcessor.class || prev.processorClass() == SpongeWildcardProcessor.class) && - prev.result() == Tristate.TRUE; - } - - @Override - public TristateResult hasPermission(TristateResult prev, String permission) { - if (prev != TristateResult.UNDEFINED) { - // Check to see if the result should be overridden - if (canOverrideWildcard(prev)) { - Permission defPerm = this.plugin.getPermissionMap().get(permission); - if (defPerm != null) { - PermissionDefault def = defPerm.getDefault(); - if (def == PermissionDefault.FALSE || this.isOp && def == PermissionDefault.NOT_OP) { - return PERMISSION_MAP_RESULT_FACTORY.result(Tristate.FALSE, "permission map (overriding wildcard): " + prev.cause()); - } - } - } - - return prev; - } - - Tristate t = this.plugin.getDefaultPermissionMap().lookupDefaultPermission(permission, this.isOp); - if (t != Tristate.UNDEFINED) { - return DEFAULT_PERMISSION_MAP_RESULT_FACTORY.result(t); - } - - Permission defPerm = this.plugin.getPermissionMap().get(permission); - if (defPerm == null) { - return TristateResult.UNDEFINED; - } - return PERMISSION_MAP_RESULT_FACTORY.result(Tristate.of(defPerm.getDefault().getValue(this.isOp))); - } -} diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/OpProcessor.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/OpProcessor.java index a4b06a18d..2eee31591 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/OpProcessor.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/OpProcessor.java @@ -25,16 +25,16 @@ package me.lucko.luckperms.bukkit.calculator; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; - import net.luckperms.api.util.Tristate; /** * Permission Processor which is added for opped users, to simply return true if * no other processors match. */ -public final class OpProcessor implements PermissionProcessor { +public final class OpProcessor extends AbstractPermissionProcessor implements PermissionProcessor { private static final TristateResult TRUE_RESULT = new TristateResult.Factory(OpProcessor.class).result(Tristate.TRUE); public static final OpProcessor INSTANCE = new OpProcessor(); @@ -44,10 +44,7 @@ private OpProcessor() { } @Override - public TristateResult hasPermission(TristateResult prev, String permission) { - if (prev != TristateResult.UNDEFINED) { - return prev; - } + public TristateResult hasPermission(String permission) { return TRUE_RESULT; } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/PermissionMapProcessor.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/PermissionMapProcessor.java new file mode 100644 index 000000000..1587c16a0 --- /dev/null +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/calculator/PermissionMapProcessor.java @@ -0,0 +1,58 @@ +/* + * 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.bukkit.calculator; + +import me.lucko.luckperms.bukkit.LPBukkitPlugin; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractOverrideWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import net.luckperms.api.util.Tristate; +import org.bukkit.permissions.Permission; + +/** + * Permission Processor for Bukkits "default" permission system. + */ +public class PermissionMapProcessor extends AbstractOverrideWildcardProcessor implements PermissionProcessor { + private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(PermissionMapProcessor.class); + + private final LPBukkitPlugin plugin; + private final boolean isOp; + + public PermissionMapProcessor(LPBukkitPlugin plugin, boolean overrideWildcards, boolean isOp) { + super(overrideWildcards); + this.plugin = plugin; + this.isOp = isOp; + } + + @Override + public TristateResult hasPermission(String permission) { + Permission defPerm = this.plugin.getPermissionMap().get(permission); + if (defPerm == null) { + return TristateResult.UNDEFINED; + } + return RESULT_FACTORY.result(Tristate.of(defPerm.getDefault().getValue(this.isOp))); + } +} diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/context/BukkitContextManager.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/context/BukkitContextManager.java index 738b3cfef..f9735bfa8 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/context/BukkitContextManager.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/context/BukkitContextManager.java @@ -25,88 +25,46 @@ package me.lucko.luckperms.bukkit.context; -import com.github.benmanes.caffeine.cache.LoadingCache; - import me.lucko.luckperms.bukkit.LPBukkitPlugin; -import me.lucko.luckperms.common.cache.LoadingMap; -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.ContextManager; -import me.lucko.luckperms.common.context.QueryOptionsCache; -import me.lucko.luckperms.common.util.CaffeineFactory; - -import net.luckperms.api.context.ImmutableContextSet; +import me.lucko.luckperms.bukkit.inject.permissible.LuckPermsPermissible; +import me.lucko.luckperms.bukkit.inject.permissible.PermissibleInjector; +import me.lucko.luckperms.common.context.manager.DetachedContextManager; +import me.lucko.luckperms.common.context.manager.QueryOptionsSupplier; import net.luckperms.api.query.OptionKey; import net.luckperms.api.query.QueryOptions; - import org.bukkit.entity.Player; +import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; import java.util.UUID; -import java.util.concurrent.TimeUnit; -public class BukkitContextManager extends ContextManager { +public class BukkitContextManager extends DetachedContextManager { public static final OptionKey OP_OPTION = OptionKey.of("op", Boolean.class); - // cache the creation of ContextsCache instances for online players with no expiry - private final LoadingMap> onlineSubjectCaches = LoadingMap.of(key -> new QueryOptionsCache<>(key, this)); - - // cache the creation of ContextsCache instances for offline players with a 1m expiry - private final LoadingCache> offlineSubjectCaches = CaffeineFactory.newBuilder() - .expireAfterAccess(1, TimeUnit.MINUTES) - .build(key -> { - QueryOptionsCache cache = this.onlineSubjectCaches.getIfPresent(key); - if (cache != null) { - return cache; - } - return new QueryOptionsCache<>(key, this); - }); - public BukkitContextManager(LPBukkitPlugin plugin) { super(plugin, Player.class, Player.class); } - public void onPlayerQuit(Player player) { - this.onlineSubjectCaches.remove(player); - } - @Override public UUID getUniqueId(Player player) { return player.getUniqueId(); } @Override - public QueryOptionsCache getCacheFor(Player subject) { - if (subject == null) { - throw new NullPointerException("subject"); - } - - if (subject.isOnline()) { - return this.onlineSubjectCaches.get(subject); - } else { - return this.offlineSubjectCaches.get(subject); - } - } - - @Override - protected void invalidateCache(Player subject) { - QueryOptionsCache cache = this.onlineSubjectCaches.getIfPresent(subject); - if (cache != null) { - cache.invalidate(); - } - - cache = this.offlineSubjectCaches.getIfPresent(subject); - if (cache != null) { - cache.invalidate(); + public @Nullable QueryOptionsSupplier getQueryOptionsSupplier(Player subject) { + Objects.requireNonNull(subject, "subject"); + LuckPermsPermissible permissible = PermissibleInjector.get(subject); + if (permissible != null) { + return permissible.getQueryOptionsSupplier(); } + return null; } @Override - public QueryOptions formQueryOptions(Player subject, ImmutableContextSet contextSet) { - QueryOptions.Builder queryOptions = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder(); + public void customizeQueryOptions(Player subject, QueryOptions.Builder builder) { if (subject.isOp()) { - queryOptions.option(OP_OPTION, true); + builder.option(OP_OPTION, true); } - - return queryOptions.context(contextSet).build(); } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/context/BukkitPlayerCalculator.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/context/BukkitPlayerCalculator.java index 15ab0693e..2b14c490d 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/context/BukkitPlayerCalculator.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/context/BukkitPlayerCalculator.java @@ -26,19 +26,16 @@ package me.lucko.luckperms.bukkit.context; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.bukkit.LPBukkitPlugin; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.util.EnumNamer; - import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextCalculator; import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; - import org.bukkit.GameMode; import org.bukkit.World; import org.bukkit.World.Environment; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/DummyPermissibleBase.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/DummyPermissibleBase.java index ae76882c1..657a25740 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/DummyPermissibleBase.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/DummyPermissibleBase.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.bukkit.inject.permissible; import me.lucko.luckperms.common.util.EmptyCollections; - import org.bukkit.permissions.PermissibleBase; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionAttachment; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/LuckPermsPermissible.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/LuckPermsPermissible.java index 8958aad92..a8f5848cb 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/LuckPermsPermissible.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/LuckPermsPermissible.java @@ -27,19 +27,16 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; - import me.lucko.luckperms.bukkit.LPBukkitPlugin; -import me.lucko.luckperms.bukkit.calculator.DefaultsProcessor; import me.lucko.luckperms.bukkit.calculator.OpProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.bukkit.calculator.PermissionMapProcessor; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.QueryOptionsCache; +import me.lucko.luckperms.common.context.manager.QueryOptionsSupplier; import me.lucko.luckperms.common.model.User; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; - import org.bukkit.entity.Player; import org.bukkit.permissions.PermissibleBase; import org.bukkit.permissions.Permission; @@ -71,7 +68,7 @@ * "Hot" method calls, (namely #hasPermission) are significantly faster than the base implementation. * * This class is **thread safe**. This means that when LuckPerms is installed on the server, - * is is safe to call Player#hasPermission asynchronously. + * it is safe to call Player#hasPermission asynchronously. */ public class LuckPermsPermissible extends PermissibleBase { @@ -96,7 +93,7 @@ public class LuckPermsPermissible extends PermissibleBase { private final LPBukkitPlugin plugin; // caches context lookups for the player - private final QueryOptionsCache queryOptionsSupplier; + private final QueryOptionsSupplier queryOptionsSupplier; // the players previous permissible. (the one they had before this one was injected) private PermissibleBase oldPermissible = null; @@ -113,7 +110,7 @@ public LuckPermsPermissible(Player player, User user, LPBukkitPlugin plugin) { this.user = Objects.requireNonNull(user, "user"); this.player = Objects.requireNonNull(player, "player"); this.plugin = Objects.requireNonNull(plugin, "plugin"); - this.queryOptionsSupplier = plugin.getContextManager().getCacheFor(player); + this.queryOptionsSupplier = plugin.getContextManager().createQueryOptionsSupplier(player); injectFakeAttachmentsList(); } @@ -143,13 +140,13 @@ public boolean isPermissionSet(@NonNull String permission) { } QueryOptions queryOptions = this.queryOptionsSupplier.getQueryOptions(); - TristateResult result = this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission, PermissionCheckEvent.Origin.PLATFORM_LOOKUP_CHECK); + TristateResult result = this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET); if (result.result() == Tristate.UNDEFINED) { return false; } // ignore matches made from looking up in the permission map (replicate bukkit behaviour) - if (result.processorClass() == DefaultsProcessor.class && "permission map".equals(result.cause())) { + if (result.processorClass() == PermissionMapProcessor.class) { return false; } @@ -173,7 +170,7 @@ public boolean hasPermission(@NonNull String permission) { } QueryOptions queryOptions = this.queryOptionsSupplier.getQueryOptions(); - return this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission, PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK).result().asBoolean(); + return this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result().asBoolean(); } @Override @@ -183,7 +180,7 @@ public boolean hasPermission(@NonNull Permission permission) { } QueryOptions queryOptions = this.queryOptionsSupplier.getQueryOptions(); - TristateResult result = this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission.getName(), PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK); + TristateResult result = this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission.getName(), CheckOrigin.PLATFORM_API_HAS_PERMISSION); // override default op handling using the Permission class we have if (result.processorClass() == OpProcessor.class && this.plugin.getConfiguration().get(ConfigKeys.APPLY_BUKKIT_DEFAULT_PERMISSIONS)) { @@ -296,7 +293,7 @@ public void recalculatePermissions() { // the query options cache when op status changes. // (#invalidate is a fast call) if (this.queryOptionsSupplier != null) { // this method is called by the super class constructor, before this class has fully initialised - this.queryOptionsSupplier.invalidate(); + this.queryOptionsSupplier.invalidateCache(); } // but we don't need to do anything else in this method, unlike the CB impl. @@ -319,6 +316,10 @@ public LPBukkitPlugin getPlugin() { return this.plugin; } + public QueryOptionsSupplier getQueryOptionsSupplier() { + return this.queryOptionsSupplier; + } + PermissibleBase getOldPermissible() { return this.oldPermissible; } @@ -412,9 +413,7 @@ public ListIterator listIterator() { @Override public PermissionAttachment remove(int index) { throw new UnsupportedOperationException(); } @Override public int indexOf(Object o) { throw new UnsupportedOperationException(); } @Override public int lastIndexOf(Object o) { throw new UnsupportedOperationException(); } - @Override - public @NonNull ListIterator listIterator(int index) { throw new UnsupportedOperationException(); } - @Override - public @NonNull List subList(int fromIndex, int toIndex) { throw new UnsupportedOperationException(); } + @Override public @NonNull ListIterator listIterator(int index) { throw new UnsupportedOperationException(); } + @Override public @NonNull List subList(int fromIndex, int toIndex) { throw new UnsupportedOperationException(); } } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/LuckPermsPermissionAttachment.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/LuckPermsPermissionAttachment.java index 0a9f10ac0..4afd48c9a 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/LuckPermsPermissionAttachment.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/LuckPermsPermissionAttachment.java @@ -26,17 +26,14 @@ package me.lucko.luckperms.bukkit.inject.permissible; import com.google.common.base.Preconditions; - import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.node.factory.NodeBuilders; - import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.query.Flag; import net.luckperms.api.query.QueryOptions; - import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionRemovedExecutor; import org.bukkit.plugin.Plugin; @@ -46,6 +43,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -239,7 +237,7 @@ public void setPermission(@NonNull String name, boolean value) { Objects.requireNonNull(name, "name is null"); Preconditions.checkArgument(!name.isEmpty(), "name is empty"); - String permission = name.toLowerCase(); + String permission = name.toLowerCase(Locale.ROOT); Boolean previous = this.perms.put(permission, value); if (previous != null && previous == value) { @@ -264,7 +262,7 @@ public void unsetPermission(@NonNull String name) { Objects.requireNonNull(name, "name is null"); Preconditions.checkArgument(!name.isEmpty(), "name is empty"); - String permission = name.toLowerCase(); + String permission = name.toLowerCase(Locale.ROOT); Boolean previous = this.perms.remove(permission); if (previous == null) { diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/MonitoredPermissibleBase.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/MonitoredPermissibleBase.java index e3c4f57c5..3f37b5386 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/MonitoredPermissibleBase.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/MonitoredPermissibleBase.java @@ -25,15 +25,13 @@ package me.lucko.luckperms.bukkit.inject.permissible; -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; import me.lucko.luckperms.common.verbose.VerboseHandler; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.util.Tristate; - import org.bukkit.permissions.PermissibleBase; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionAttachment; @@ -69,8 +67,8 @@ public MonitoredPermissibleBase(LuckPermsPlugin plugin, PermissibleBase delegate this.initialised = true; } - private void logCheck(PermissionCheckEvent.Origin origin, String permission, boolean result) { - this.plugin.getVerboseHandler().offerPermissionCheckEvent(origin, this.verboseCheckTarget, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.of(Tristate.of(result))); + private void logCheck(CheckOrigin origin, String permission, boolean result) { + this.plugin.getVerboseHandler().offerPermissionCheckEvent(origin, this.verboseCheckTarget, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.forMonitoredResult(Tristate.of(result))); this.plugin.getPermissionRegistry().offer(permission); } @@ -85,7 +83,7 @@ public boolean isPermissionSet(@NonNull String permission) { } final boolean result = this.delegate.isPermissionSet(permission); - logCheck(PermissionCheckEvent.Origin.PLATFORM_LOOKUP_CHECK, permission, result); + logCheck(CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET, permission, result); return result; } @@ -96,7 +94,7 @@ public boolean isPermissionSet(@NonNull Permission permission) { } final boolean result = this.delegate.isPermissionSet(permission); - logCheck(PermissionCheckEvent.Origin.PLATFORM_LOOKUP_CHECK, permission.getName(), result); + logCheck(CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET, permission.getName(), result); return result; } @@ -107,7 +105,7 @@ public boolean hasPermission(@NonNull String permission) { } final boolean result = this.delegate.hasPermission(permission); - logCheck(PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK, permission, result); + logCheck(CheckOrigin.PLATFORM_API_HAS_PERMISSION, permission, result); return result; } @@ -118,7 +116,7 @@ public boolean hasPermission(@NonNull Permission permission) { } final boolean result = this.delegate.hasPermission(permission); - logCheck(PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK, permission.getName(), result); + logCheck(CheckOrigin.PLATFORM_API_HAS_PERMISSION, permission.getName(), result); return result; } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/PermissibleInjector.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/PermissibleInjector.java index d7e819a2c..0cdab7d1b 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/PermissibleInjector.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/PermissibleInjector.java @@ -27,10 +27,10 @@ import me.lucko.luckperms.bukkit.util.CraftBukkitImplementation; import me.lucko.luckperms.common.plugin.logging.PluginLogger; - import org.bukkit.entity.Player; import org.bukkit.permissions.PermissibleBase; import org.bukkit.permissions.PermissionAttachment; +import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.reflect.Field; import java.util.List; @@ -162,6 +162,19 @@ public static void uninject(Player player, boolean dummy) throws Exception { } } + public static @Nullable LuckPermsPermissible get(Player player) { + PermissibleBase permissibleBase; + try { + permissibleBase = (PermissibleBase) HUMAN_ENTITY_PERMISSIBLE_FIELD.get(player); + } catch (IllegalAccessException e) { + return null; + } + if (permissibleBase instanceof LuckPermsPermissible) { + return (LuckPermsPermissible) permissibleBase; + } + return null; + } + public static void checkInjected(Player player, PluginLogger logger) { PermissibleBase permissibleBase; try { diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/PermissibleMonitoringInjector.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/PermissibleMonitoringInjector.java index d55753b04..c223c15d2 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/PermissibleMonitoringInjector.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/permissible/PermissibleMonitoringInjector.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.bukkit.LPBukkitPlugin; import me.lucko.luckperms.bukkit.util.CraftBukkitImplementation; - import org.bukkit.command.ConsoleCommandSender; import org.bukkit.permissions.PermissibleBase; import org.bukkit.permissions.ServerOperator; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorDefaultsMap.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorDefaultsMap.java index 68b2a9b81..346d29fcb 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorDefaultsMap.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorDefaultsMap.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.bukkit.inject.server; import me.lucko.luckperms.bukkit.LPBukkitPlugin; - import org.bukkit.permissions.Permission; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.SimplePluginManager; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorPermissionMap.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorPermissionMap.java index bda31d3e8..c6708bed9 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorPermissionMap.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorPermissionMap.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.bukkit.inject.server; import me.lucko.luckperms.bukkit.LPBukkitPlugin; - import org.bukkit.permissions.Permission; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.SimplePluginManager; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorSubscriptionMap.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorSubscriptionMap.java index fd65b1fb8..1afc98a2b 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorSubscriptionMap.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/InjectorSubscriptionMap.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.bukkit.inject.server; import me.lucko.luckperms.bukkit.LPBukkitPlugin; - import org.bukkit.permissions.Permissible; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.SimplePluginManager; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/LuckPermsDefaultsMap.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/LuckPermsDefaultsMap.java index b1d9b93d4..91b5f9a0d 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/LuckPermsDefaultsMap.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/LuckPermsDefaultsMap.java @@ -30,12 +30,9 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; - import me.lucko.luckperms.bukkit.LPBukkitPlugin; import me.lucko.luckperms.common.cache.Cache; - import net.luckperms.api.util.Tristate; - import org.bukkit.permissions.Permission; import org.bukkit.plugin.PluginManager; import org.checkerframework.checker.nullness.qual.NonNull; @@ -43,6 +40,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -140,7 +138,7 @@ private final class DefaultsCache extends Cache> { protected @NonNull Map supply() { Map builder = new HashMap<>(); for (Permission perm : LuckPermsDefaultsMap.this.get(this.op)) { - String name = perm.getName().toLowerCase(); + String name = perm.getName().toLowerCase(Locale.ROOT); builder.put(name, true); for (Map.Entry child : LuckPermsDefaultsMap.this.plugin.getPermissionMap().getChildPermissions(name, true).entrySet()) { builder.putIfAbsent(child.getKey(), child.getValue()); diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/LuckPermsPermissionMap.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/LuckPermsPermissionMap.java index bfd4f46cb..01579398d 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/LuckPermsPermissionMap.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/inject/server/LuckPermsPermissionMap.java @@ -27,11 +27,9 @@ import com.google.common.collect.ForwardingMap; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.cache.LoadingMap; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.treeview.PermissionRegistry; - import org.bukkit.permissions.Permission; import org.bukkit.plugin.PluginManager; import org.checkerframework.checker.nullness.qual.NonNull; @@ -40,6 +38,7 @@ import java.lang.reflect.Field; import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -180,7 +179,7 @@ private void resolveChildren(Map accumulator, Map { public BukkitAutoOpListener(LPBukkitPlugin plugin) { - this.plugin = plugin; + super(plugin, plugin.getContextManager(), Player.class); } @Override - public void bind(EventBus bus) { - bus.subscribe(UserDataRecalculateEvent.class, this::onUserDataRecalculate); - bus.subscribe(ContextUpdateEvent.class, this::onContextUpdate); - } - - private void onUserDataRecalculate(UserDataRecalculateEvent e) { - User user = ApiUser.cast(e.getUser()); - this.plugin.getBootstrap().getPlayer(user.getUniqueId()).ifPresent(p -> refreshAutoOp(p, false)); + protected boolean isServerAvailable() { + return !this.plugin.getBootstrap().isServerStopping(); } - private void onContextUpdate(ContextUpdateEvent e) { - e.getSubject(Player.class).ifPresent(p -> refreshAutoOp(p, true)); + @Override + protected UUID getUniqueId(Player player) { + return player.getUniqueId(); } - private void refreshAutoOp(Player player, boolean callerIsSync) { - if (!callerIsSync && this.plugin.getBootstrap().isServerStopping()) { - return; - } - - User user = this.plugin.getUserManager().getIfLoaded(player.getUniqueId()); - - boolean value; - if (user != null) { - QueryOptions queryOptions = this.plugin.getContextManager().getQueryOptions(player); - Map permData = user.getCachedData().getPermissionData(queryOptions).getPermissionMap(); - value = permData.getOrDefault(NODE, false); - } else { - value = false; - } - + @Override + protected void setOp(Player player, boolean value, boolean callerIsSync) { if (callerIsSync) { player.setOp(value); } else { - this.plugin.getBootstrap().getScheduler().executeSync(() -> player.setOp(value)); + this.plugin.getBootstrap().getScheduler().executeSync(player, () -> player.setOp(value)); } } - } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitCommandListUpdater.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitCommandListUpdater.java index 330b55e96..f6292b7ba 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitCommandListUpdater.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitCommandListUpdater.java @@ -25,26 +25,17 @@ package me.lucko.luckperms.bukkit.listeners; -import com.github.benmanes.caffeine.cache.LoadingCache; - +import me.lucko.luckperms.bukkit.LPBukkitBootstrap; import me.lucko.luckperms.bukkit.LPBukkitPlugin; -import me.lucko.luckperms.common.cache.BufferedRequest; -import me.lucko.luckperms.common.event.LuckPermsEventListener; -import me.lucko.luckperms.common.util.CaffeineFactory; - -import net.luckperms.api.event.EventBus; -import net.luckperms.api.event.context.ContextUpdateEvent; -import net.luckperms.api.event.user.UserDataRecalculateEvent; - +import me.lucko.luckperms.common.event.listeners.AbstractCommandListUpdater; import org.bukkit.entity.Player; import java.util.UUID; -import java.util.concurrent.TimeUnit; /** * Calls {@link Player#updateCommands()} when a players permissions change. */ -public class BukkitCommandListUpdater implements LuckPermsEventListener { +public class BukkitCommandListUpdater extends AbstractCommandListUpdater { public static boolean isSupported() { try { @@ -55,64 +46,30 @@ public static boolean isSupported() { } } - private final LPBukkitPlugin plugin; - private final LoadingCache sendingBuffers = CaffeineFactory.newBuilder() - .expireAfterAccess(10, TimeUnit.SECONDS) - .build(SendBuffer::new); - public BukkitCommandListUpdater(LPBukkitPlugin plugin) { - this.plugin = plugin; + super(plugin, Player.class); } @Override - public void bind(EventBus bus) { - bus.subscribe(UserDataRecalculateEvent.class, this::onUserDataRecalculate); - bus.subscribe(ContextUpdateEvent.class, this::onContextUpdate); - } - - private void onUserDataRecalculate(UserDataRecalculateEvent e) { - requestUpdate(e.getUser().getUniqueId()); - } - - private void onContextUpdate(ContextUpdateEvent e) { - e.getSubject(Player.class).ifPresent(p -> requestUpdate(p.getUniqueId())); + protected boolean isServerAvailable() { + return !this.plugin.getBootstrap().isServerStopping(); } - private void requestUpdate(UUID uniqueId) { - if (this.plugin.getBootstrap().isServerStopping()) { - return; - } - - if (!this.plugin.getBootstrap().isPlayerOnline(uniqueId)) { - return; - } - - // Buffer the request to send a commands update. - this.sendingBuffers.get(uniqueId).request(); + @Override + protected UUID getUniqueId(Player player) { + return player.getUniqueId(); } - // Called when the buffer times out. - private void sendUpdate(UUID uniqueId) { - if (this.plugin.getBootstrap().isServerStopping()) { + @Override + protected void sendCommandListUpdate(UUID uniqueId) { + LPBukkitBootstrap bootstrap = this.plugin.getBootstrap(); + if (bootstrap.isServerStopping()) { return; } - - this.plugin.getBootstrap().getScheduler().sync() - .execute(() -> this.plugin.getBootstrap().getPlayer(uniqueId).ifPresent(Player::updateCommands)); - } - - private final class SendBuffer extends BufferedRequest { - private final UUID uniqueId; - - SendBuffer(UUID uniqueId) { - super(500, TimeUnit.MILLISECONDS, BukkitCommandListUpdater.this.plugin.getBootstrap().getScheduler()); - this.uniqueId = uniqueId; - } - @Override - protected Void perform() { - sendUpdate(this.uniqueId); - return null; + Player player = bootstrap.getPlayer(uniqueId).orElse(null); + if (player != null) { + bootstrap.getScheduler().executeSync(player, player::updateCommands); } } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitConnectionListener.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitConnectionListener.java index 9dbbd5b24..4d4f3c2b3 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitConnectionListener.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitConnectionListener.java @@ -34,10 +34,8 @@ import me.lucko.luckperms.common.locale.TranslationManager; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; - import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; - import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -242,7 +240,7 @@ public void onPlayerQuit(PlayerQuitEvent e) { // perform unhooking from bukkit objects 1 tick later. // this allows plugins listening after us on MONITOR to still have intact permissions data - this.plugin.getBootstrap().getServer().getScheduler().runTaskLater(this.plugin.getLoader(), () -> { + this.plugin.getBootstrap().getScheduler().executeSync(() -> { // Remove the custom permissible try { PermissibleInjector.uninject(player, true); @@ -255,10 +253,7 @@ public void onPlayerQuit(PlayerQuitEvent e) { if (this.plugin.getConfiguration().get(ConfigKeys.AUTO_OP)) { player.setOp(false); } - - // remove their contexts cache - this.plugin.getContextManager().onPlayerQuit(player); - }, 1L); + }); } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitPlatformListener.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitPlatformListener.java index 94a7501cf..20b64405b 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitPlatformListener.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/listeners/BukkitPlatformListener.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.bukkit.LPBukkitPlugin; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.locale.Message; - import org.bukkit.command.CommandSender; import org.bukkit.event.Cancellable; import org.bukkit.event.EventHandler; @@ -37,11 +36,25 @@ import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.event.server.RemoteServerCommandEvent; import org.bukkit.event.server.ServerCommandEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; import java.util.regex.Pattern; public class BukkitPlatformListener implements Listener { - private static final Pattern OP_COMMAND_PATTERN = Pattern.compile("^/?(\\w+:)?(deop|op)( .*)?$"); + private static final Pattern OP_COMMAND_PATTERN = Pattern.compile("^/?(\\w+:)?(deop|op)( .*)?$", Pattern.CASE_INSENSITIVE); + + private static final boolean PLUGIN_DESCRIPTION_FILE_PROVIDES_SUPPORTED; + + static { + boolean supported = false; + try { + PluginDescriptionFile.class.getMethod("getProvides"); + supported = true; + } catch (NoSuchMethodException ignored) { + } + PLUGIN_DESCRIPTION_FILE_PROVIDES_SUPPORTED = supported; + } private final LPBukkitPlugin plugin; @@ -51,17 +64,17 @@ public BukkitPlatformListener(LPBukkitPlugin plugin) { @EventHandler(ignoreCancelled = true) public void onPlayerCommand(PlayerCommandPreprocessEvent e) { - handleCommand(e.getPlayer(), e.getMessage().toLowerCase(), e); + handleCommand(e.getPlayer(), e.getMessage(), e); } @EventHandler(ignoreCancelled = true) public void onServerCommand(ServerCommandEvent e) { - handleCommand(e.getSender(), e.getCommand().toLowerCase(), e); + handleCommand(e.getSender(), e.getCommand(), e); } @EventHandler(ignoreCancelled = true) public void onRemoteServerCommand(RemoteServerCommandEvent e) { - handleCommand(e.getSender(), e.getCommand().toLowerCase(), e); + handleCommand(e.getSender(), e.getCommand(), e); } private void handleCommand(CommandSender sender, String cmdLine, Cancellable event) { @@ -81,7 +94,11 @@ private void handleCommand(CommandSender sender, String cmdLine, Cancellable eve @EventHandler public void onPluginEnable(PluginEnableEvent e) { - if (e.getPlugin().getName().equalsIgnoreCase("Vault")) { + Plugin p = e.getPlugin(); + boolean shouldHook = p.getName().equalsIgnoreCase("Vault") || + (PLUGIN_DESCRIPTION_FILE_PROVIDES_SUPPORTED && p.getDescription().getProvides().contains("Vault")); + + if (shouldHook) { this.plugin.tryVaultHook(true); } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/BukkitMessagingFactory.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/BukkitMessagingFactory.java index 379ba925a..fc01ccdf2 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/BukkitMessagingFactory.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/BukkitMessagingFactory.java @@ -29,11 +29,9 @@ import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; import me.lucko.luckperms.common.messaging.MessagingFactory; - import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; - import org.checkerframework.checker.nullness.qual.NonNull; public class BukkitMessagingFactory extends MessagingFactory { diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/LilyPadMessenger.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/LilyPadMessenger.java index 0fd24647e..57d476ee1 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/LilyPadMessenger.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/LilyPadMessenger.java @@ -25,19 +25,16 @@ package me.lucko.luckperms.bukkit.messaging; -import me.lucko.luckperms.bukkit.LPBukkitPlugin; - -import net.luckperms.api.messenger.IncomingMessageConsumer; -import net.luckperms.api.messenger.Messenger; -import net.luckperms.api.messenger.message.OutgoingMessage; - -import org.checkerframework.checker.nullness.qual.NonNull; - import lilypad.client.connect.api.Connect; import lilypad.client.connect.api.event.EventListener; import lilypad.client.connect.api.event.MessageEvent; import lilypad.client.connect.api.request.RequestException; import lilypad.client.connect.api.request.impl.MessageRequest; +import me.lucko.luckperms.bukkit.LPBukkitPlugin; +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.luckperms.api.messenger.message.OutgoingMessage; +import org.checkerframework.checker.nullness.qual.NonNull; import java.nio.charset.StandardCharsets; import java.util.Collections; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/PluginMessageMessenger.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/PluginMessageMessenger.java index 9cf47f71d..c119ab8c0 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/PluginMessageMessenger.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/messaging/PluginMessageMessenger.java @@ -26,16 +26,10 @@ package me.lucko.luckperms.bukkit.messaging; import com.google.common.collect.Iterables; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; - import me.lucko.luckperms.bukkit.LPBukkitPlugin; - +import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; -import net.luckperms.api.messenger.message.OutgoingMessage; - import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.PluginMessageListener; import org.bukkit.scheduler.BukkitRunnable; @@ -46,15 +40,12 @@ /** * An implementation of {@link Messenger} using the plugin messaging channels. */ -public class PluginMessageMessenger implements Messenger, PluginMessageListener { - private static final String CHANNEL = "luckperms:update"; - +public class PluginMessageMessenger extends AbstractPluginMessageMessenger implements PluginMessageListener { private final LPBukkitPlugin plugin; - private final IncomingMessageConsumer consumer; public PluginMessageMessenger(LPBukkitPlugin plugin, IncomingMessageConsumer consumer) { + super(consumer); this.plugin = plugin; - this.consumer = consumer; } public void init() { @@ -69,11 +60,7 @@ public void close() { } @Override - public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF(outgoingMessage.asEncodedString()); - byte[] data = out.toByteArray(); - + protected void sendOutgoingMessage(byte[] buf) { new BukkitRunnable() { @Override public void run() { @@ -83,21 +70,18 @@ public void run() { return; } - p.sendPluginMessage(PluginMessageMessenger.this.plugin.getLoader(), CHANNEL, data); + p.sendPluginMessage(PluginMessageMessenger.this.plugin.getLoader(), CHANNEL, buf); cancel(); } }.runTaskTimer(this.plugin.getLoader(), 1L, 100L); } @Override - public void onPluginMessageReceived(String s, @NonNull Player player, @NonNull byte[] bytes) { - if (!s.equals(CHANNEL)) { + public void onPluginMessageReceived(String channel, @NonNull Player player, byte @NonNull [] message) { + if (!channel.equals(CHANNEL)) { return; } - ByteArrayDataInput in = ByteStreams.newDataInput(bytes); - String msg = in.readUTF(); - - this.consumer.consumeIncomingMessageAsString(msg); + handleIncomingMessage(message); } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/NullSafeConsoleCommandSender.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/NullSafeConsoleCommandSender.java index 11f6f3984..49726c7c9 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/NullSafeConsoleCommandSender.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/NullSafeConsoleCommandSender.java @@ -25,6 +25,7 @@ package me.lucko.luckperms.bukkit.util; +import net.kyori.adventure.text.Component; import org.bukkit.ChatColor; import org.bukkit.Server; import org.bukkit.command.ConsoleCommandSender; @@ -35,9 +36,12 @@ import org.bukkit.permissions.PermissionAttachmentInfo; import org.bukkit.plugin.Plugin; import org.checkerframework.checker.nullness.qual.NonNull; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Optional; import java.util.Set; +import java.util.UUID; /** * The {@link Server#getConsoleSender()} method returns null during onEnable @@ -109,12 +113,16 @@ public boolean isOp() { // just throw UnsupportedOperationException - we never use any of these methods @Override public @NonNull Spigot spigot() { throw new UnsupportedOperationException(); } + @Override public void sendMessage(@Nullable UUID uuid, @NotNull String s) { throw new UnsupportedOperationException(); } + @Override public void sendMessage(@Nullable UUID uuid, @NotNull String... strings) { throw new UnsupportedOperationException(); } + @Override public @NotNull Component name() { throw new UnsupportedOperationException(); } @Override public boolean isConversing() { throw new UnsupportedOperationException(); } @Override public void acceptConversationInput(@NonNull String s) { throw new UnsupportedOperationException(); } @Override public boolean beginConversation(@NonNull Conversation conversation) { throw new UnsupportedOperationException(); } @Override public void abandonConversation(@NonNull Conversation conversation) { throw new UnsupportedOperationException(); } @Override public void abandonConversation(@NonNull Conversation conversation, @NonNull ConversationAbandonedEvent conversationAbandonedEvent) { throw new UnsupportedOperationException(); } @Override public void sendRawMessage(@NonNull String s) { throw new UnsupportedOperationException(); } + @Override public void sendRawMessage(@Nullable UUID uuid, @NotNull String s) { throw new UnsupportedOperationException(); } @Override public @NonNull PermissionAttachment addAttachment(@NonNull Plugin plugin, @NonNull String s, boolean b) { throw new UnsupportedOperationException(); } @Override public @NonNull PermissionAttachment addAttachment(@NonNull Plugin plugin) { throw new UnsupportedOperationException(); } @Override public PermissionAttachment addAttachment(@NonNull Plugin plugin, @NonNull String s, boolean b, int i) { throw new UnsupportedOperationException(); } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PaperAdventureBridge.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PaperAdventureBridge.java new file mode 100644 index 000000000..213871df7 --- /dev/null +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PaperAdventureBridge.java @@ -0,0 +1,84 @@ +/* + * 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.bukkit.util; + +import com.google.gson.JsonElement; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import org.bukkit.command.CommandSender; + +import java.lang.reflect.Method; + +public final class PaperAdventureBridge { + + public static final PaperAdventureBridge INSTANCE; + static { + PaperAdventureBridge bridge = null; + try { + bridge = new PaperAdventureBridge(); + } catch (Exception e) { + // ignore + } + INSTANCE = bridge; + } + + private final Method serializerDeserialize; + private final Method sendMessage; + private final Object serializerInstance; + + private PaperAdventureBridge() throws Exception { + String adventurePkg = "net.kyo".concat("ri.adventure."); + Class audienceClass = Class.forName(adventurePkg + "audience.Audience"); + Class componentClass = Class.forName(adventurePkg + "text.Component"); + Class serializerClass = Class.forName(adventurePkg + "text.serializer.gson.GsonComponentSerializer"); + + if (!audienceClass.isAssignableFrom(CommandSender.class)) { + throw new IllegalStateException("CommandSender does not implement Audience"); + } + + this.serializerDeserialize = serializerClass.getMethod("deserializeFromTree", JsonElement.class); + this.sendMessage = audienceClass.getMethod("sendMessage", componentClass); + this.serializerInstance = serializerClass.getMethod("gson").invoke(null); + } + + public Object toPlatformComponent(Component component) { + JsonElement json = GsonComponentSerializer.gson().serializeToTree(component); + try { + return this.serializerDeserialize.invoke(this.serializerInstance, json); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + public void sendMessage(CommandSender audience, Component message) { + try { + this.sendMessage.invoke(audience, toPlatformComponent(message)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PlayerLocaleUtil.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PlayerLocaleUtil.java index 0c169b311..305f10ccc 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PlayerLocaleUtil.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PlayerLocaleUtil.java @@ -25,42 +25,55 @@ package me.lucko.luckperms.bukkit.util; +import me.lucko.luckperms.common.locale.TranslationManager; import org.bukkit.entity.Player; import java.lang.reflect.Method; +import java.util.Locale; import java.util.function.Function; public final class PlayerLocaleUtil { private PlayerLocaleUtil() {} - private static final Function GET_LOCALE_FUNCTION; + private static final Function GET_LOCALE_FUNCTION = getLocaleFunction(); - static { - Function function; + private static Function getLocaleFunction() { + // modern Paper + try { + Player.class.getMethod("locale"); + return Player::locale; + } catch (ReflectiveOperationException e) { + // ignore + } + + // modern bukkit try { - // modern bukkit Player.class.getMethod("getLocale"); - function = Player::getLocale; - } catch (ReflectiveOperationException ex) { - try { - // legacy spigot method - Method legacyMethod = Player.Spigot.class.getMethod("getLocale"); - function = player -> { - try { - return (String) legacyMethod.invoke(player.spigot()); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - }; - } catch (ReflectiveOperationException e) { - // fallback - function = player -> null; - } + return player -> TranslationManager.parseLocale(player.getLocale()); + } catch (ReflectiveOperationException e) { + // ignore } - GET_LOCALE_FUNCTION = function; + + // legacy spigot method + try { + Method legacyMethod = Player.Spigot.class.getMethod("getLocale"); + return player -> { + try { + String localeString = (String) legacyMethod.invoke(player.spigot()); + return TranslationManager.parseLocale(localeString); + } catch (ReflectiveOperationException ex) { + throw new RuntimeException(ex); + } + }; + } catch (ReflectiveOperationException e) { + // ignore + } + + // fallback + return player -> null; } - public static String getLocale(Player player) { + public static Locale getLocale(Player player) { return GET_LOCALE_FUNCTION.apply(player); } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PluginManagerUtil.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PluginManagerUtil.java index 421fcc4e8..47eea2edb 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PluginManagerUtil.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/util/PluginManagerUtil.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.bukkit.util; import com.google.common.graph.MutableGraph; - import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.SimplePluginManager; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/AbstractVaultChat.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/AbstractVaultChat.java index 37164ac91..81e947185 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/AbstractVaultChat.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/AbstractVaultChat.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.bukkit.vault; import net.milkbowl.vault.chat.Chat; - import org.bukkit.OfflinePlayer; import org.bukkit.World; import org.bukkit.entity.Player; diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/AbstractVaultPermission.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/AbstractVaultPermission.java index e22b3a1b5..ed17bc239 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/AbstractVaultPermission.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/AbstractVaultPermission.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.bukkit.vault; import net.milkbowl.vault.permission.Permission; - import org.bukkit.OfflinePlayer; import org.bukkit.World; import org.bukkit.entity.Player; @@ -79,6 +78,8 @@ public final boolean hasGroupSupport() { public abstract boolean userHasPermission(String world, UUID uuid, String permission); public abstract boolean userAddPermission(String world, UUID uuid, String permission); public abstract boolean userRemovePermission(String world, UUID uuid, String permission); + public abstract boolean userAddTransient(String world, UUID uuid, String permission); + public abstract boolean userRemoveTransient(String world, UUID uuid, String permission); public abstract boolean userInGroup(String world, UUID uuid, String group); public abstract boolean userAddGroup(String world, UUID uuid, String group); public abstract boolean userRemoveGroup(String world, UUID uuid, String group); @@ -181,7 +182,7 @@ public final boolean playerAdd(String world, OfflinePlayer player, String permis public final boolean playerAdd(Player player, String permission) { Objects.requireNonNull(player, "player"); Objects.requireNonNull(permission, "permission"); - return userAddPermission(convertWorld(player), ((OfflinePlayer) player).getUniqueId(), permission); + return userAddPermission(convertWorld(player), player.getUniqueId(), permission); } @Override @@ -209,7 +210,63 @@ public final boolean playerRemove(World world, String player, String permission) public final boolean playerRemove(Player player, String permission) { Objects.requireNonNull(player, "player"); Objects.requireNonNull(permission, "permission"); - return userRemovePermission(convertWorld(player), ((OfflinePlayer) player).getUniqueId(), permission); + return userRemovePermission(convertWorld(player), player.getUniqueId(), permission); + } + + @Override + public boolean playerAddTransient(OfflinePlayer player, String permission) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(permission, "permission"); + return userAddTransient(null, player.getUniqueId(), permission); + } + + @Override + public final boolean playerAddTransient(Player player, String permission) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(permission, "permission"); + return userAddTransient(convertWorld(player), player.getUniqueId(), permission); + } + + @Override + public final boolean playerAddTransient(String world, OfflinePlayer player, String permission) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(permission, "permission"); + return userAddTransient(convertWorld(world), player.getUniqueId(), permission); + } + + @Override + public final boolean playerAddTransient(String world, Player player, String permission) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(permission, "permission"); + return userAddTransient(convertWorld(world), player.getUniqueId(), permission); + } + + @Override + public boolean playerRemoveTransient(OfflinePlayer player, String permission) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(permission, "permission"); + return userRemoveTransient(null, player.getUniqueId(), permission); + } + + @Override + public final boolean playerRemoveTransient(Player player, String permission) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(permission, "permission"); + return userRemoveTransient(convertWorld(player), player.getUniqueId(), permission); + } + + @Override + public final boolean playerRemoveTransient(String world, OfflinePlayer player, String permission) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(permission, "permission"); + return userRemoveTransient(convertWorld(world), player.getUniqueId(), permission); + } + + @Override + public final boolean playerRemoveTransient(String world, Player player, String permission) { + Objects.requireNonNull(player, "player"); + Objects.requireNonNull(permission, "permission"); + return userRemoveTransient(convertWorld(world), player.getUniqueId(), permission); } @Override @@ -279,7 +336,7 @@ public final boolean playerInGroup(String world, OfflinePlayer player, String gr public final boolean playerInGroup(Player player, String group) { Objects.requireNonNull(player, "player"); Objects.requireNonNull(group, "group"); - return userInGroup(convertWorld(player), ((OfflinePlayer) player).getUniqueId(), group); + return userInGroup(convertWorld(player), player.getUniqueId(), group); } @Override @@ -307,7 +364,7 @@ public final boolean playerAddGroup(String world, OfflinePlayer player, String g public final boolean playerAddGroup(Player player, String group) { Objects.requireNonNull(player, "player"); Objects.requireNonNull(group, "group"); - return userAddGroup(convertWorld(player), ((OfflinePlayer) player).getUniqueId(), group); + return userAddGroup(convertWorld(player), player.getUniqueId(), group); } @Override @@ -334,7 +391,7 @@ public final boolean playerRemoveGroup(String world, OfflinePlayer player, Strin @Override public final boolean playerRemoveGroup(Player player, String group) { Objects.requireNonNull(player, "player"); - return userRemoveGroup(convertWorld(player), ((OfflinePlayer) player).getUniqueId(), group); + return userRemoveGroup(convertWorld(player), player.getUniqueId(), group); } @Override @@ -358,7 +415,7 @@ public final String[] getPlayerGroups(String world, OfflinePlayer player) { @Override public final String[] getPlayerGroups(Player player) { Objects.requireNonNull(player, "player"); - return userGetGroups(convertWorld(player), ((OfflinePlayer) player).getUniqueId()); + return userGetGroups(convertWorld(player), player.getUniqueId()); } @Override @@ -382,7 +439,7 @@ public final String getPrimaryGroup(String world, OfflinePlayer player) { @Override public final String getPrimaryGroup(Player player) { Objects.requireNonNull(player, "player"); - return userGetPrimaryGroup(convertWorld(player), ((OfflinePlayer) player).getUniqueId()); + return userGetPrimaryGroup(convertWorld(player), player.getUniqueId()); } } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/LuckPermsVaultChat.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/LuckPermsVaultChat.java index 0e539b8a1..47e9ac4b1 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/LuckPermsVaultChat.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/LuckPermsVaultChat.java @@ -26,19 +26,18 @@ package me.lucko.luckperms.bukkit.vault; import com.google.common.base.Strings; - import me.lucko.luckperms.bukkit.LPBukkitPlugin; import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator; import me.lucko.luckperms.common.cacheddata.type.MetaCache; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.cacheddata.type.MonitoredMetaCache; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.node.types.Meta; import me.lucko.luckperms.common.node.types.Prefix; import me.lucko.luckperms.common.node.types.Suffix; import me.lucko.luckperms.common.query.QueryOptionsImpl; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataType; @@ -49,6 +48,7 @@ import net.luckperms.api.query.QueryOptions; import net.milkbowl.vault.chat.Chat; +import java.util.Locale; import java.util.Objects; import java.util.UUID; @@ -96,7 +96,7 @@ public String getUserChatPrefix(String world, UUID uuid) { PermissionHolder user = this.vaultPermission.lookupUser(uuid); QueryOptions queryOptions = this.vaultPermission.getQueryOptions(uuid, world); MetaCache metaData = user.getCachedData().getMetaData(queryOptions); - return Strings.nullToEmpty(metaData.getPrefix(MetaCheckEvent.Origin.THIRD_PARTY_API)); + return Strings.nullToEmpty(metaData.getPrefix(CheckOrigin.THIRD_PARTY_API).result()); } @Override @@ -106,7 +106,7 @@ public String getUserChatSuffix(String world, UUID uuid) { PermissionHolder user = this.vaultPermission.lookupUser(uuid); QueryOptions queryOptions = this.vaultPermission.getQueryOptions(uuid, world); MetaCache metaData = user.getCachedData().getMetaData(queryOptions); - return Strings.nullToEmpty(metaData.getSuffix(MetaCheckEvent.Origin.THIRD_PARTY_API)); + return Strings.nullToEmpty(metaData.getSuffix(CheckOrigin.THIRD_PARTY_API).result()); } @Override @@ -138,8 +138,8 @@ public String getUserMeta(String world, UUID uuid, String key) { PermissionHolder user = this.vaultPermission.lookupUser(uuid); QueryOptions queryOptions = this.vaultPermission.getQueryOptions(uuid, world); - MetaCache metaData = user.getCachedData().getMetaData(queryOptions); - return metaData.getMetaValue(key, MetaCheckEvent.Origin.THIRD_PARTY_API); + MonitoredMetaCache metaData = user.getCachedData().getMetaData(queryOptions); + return metaData.getMetaValue(key, CheckOrigin.THIRD_PARTY_API).result(); } @Override @@ -157,21 +157,21 @@ public void setUserMeta(String world, UUID uuid, String key, Object value) { @Override public String getGroupChatPrefix(String world, String name) { Objects.requireNonNull(name, "name"); - MetaCache metaData = getGroupMetaCache(name, world); + MonitoredMetaCache metaData = getGroupMetaCache(name, world); if (metaData == null) { return null; } - return Strings.nullToEmpty(metaData.getPrefix(MetaCheckEvent.Origin.THIRD_PARTY_API)); + return Strings.nullToEmpty(metaData.getPrefix(CheckOrigin.THIRD_PARTY_API).result()); } @Override public String getGroupChatSuffix(String world, String name) { Objects.requireNonNull(name, "name"); - MetaCache metaData = getGroupMetaCache(name, world); + MonitoredMetaCache metaData = getGroupMetaCache(name, world); if (metaData == null) { return null; } - return Strings.nullToEmpty(metaData.getSuffix(MetaCheckEvent.Origin.THIRD_PARTY_API)); + return Strings.nullToEmpty(metaData.getSuffix(CheckOrigin.THIRD_PARTY_API).result()); } @Override @@ -198,11 +198,11 @@ public void setGroupChatSuffix(String world, String name, String suffix) { public String getGroupMeta(String world, String name, String key) { Objects.requireNonNull(name, "name"); Objects.requireNonNull(key, "key"); - MetaCache metaData = getGroupMetaCache(name, world); + MonitoredMetaCache metaData = getGroupMetaCache(name, world); if (metaData == null) { return null; } - return metaData.getMetaValue(key, MetaCheckEvent.Origin.THIRD_PARTY_API); + return metaData.getMetaValue(key, CheckOrigin.THIRD_PARTY_API).result(); } @Override @@ -222,7 +222,7 @@ private Group getGroup(String name) { return this.plugin.getGroupManager().getByDisplayName(name); } - private MetaCache getGroupMetaCache(String name, String world) { + private MonitoredMetaCache getGroupMetaCache(String name, String world) { Group group = getGroup(name); if (group == null) { return null; @@ -254,7 +254,7 @@ private void setChatMeta(PermissionHolder holder, ChatMetaType type, String valu private void setMeta(PermissionHolder holder, String key, Object value, String world) { if (key.equalsIgnoreCase(Prefix.NODE_KEY) || key.equalsIgnoreCase(Suffix.NODE_KEY)) { - setChatMeta(holder, ChatMetaType.valueOf(key.toUpperCase()), value == null ? null : value.toString(), world); + setChatMeta(holder, ChatMetaType.valueOf(key.toUpperCase(Locale.ROOT)), value == null ? null : value.toString(), world); return; } @@ -277,7 +277,7 @@ private void setMeta(PermissionHolder holder, String key, Object value, String w private QueryOptions createQueryOptionsForWorldSet(String world) { ImmutableContextSet.Builder context = new ImmutableContextSetImpl.BuilderImpl(); if (world != null && !world.isEmpty() && !world.equalsIgnoreCase("global")) { - context.add(DefaultContextKeys.WORLD_KEY, world.toLowerCase()); + context.add(DefaultContextKeys.WORLD_KEY, world.toLowerCase(Locale.ROOT)); } context.add(DefaultContextKeys.SERVER_KEY, this.vaultPermission.getVaultServer()); diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/LuckPermsVaultPermission.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/LuckPermsVaultPermission.java index abb24d214..d5e8860b5 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/LuckPermsVaultPermission.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/LuckPermsVaultPermission.java @@ -26,14 +26,14 @@ package me.lucko.luckperms.bukkit.vault; import com.google.common.base.Preconditions; - import me.lucko.luckperms.bukkit.LPBukkitPlugin; import me.lucko.luckperms.bukkit.context.BukkitContextManager; -import me.lucko.luckperms.common.cacheddata.type.MetaCache; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.type.MonitoredMetaCache; import me.lucko.luckperms.common.cacheddata.type.PermissionCache; import me.lucko.luckperms.common.calculator.processor.DirectProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.LuckPermsConfiguration; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.PermissionHolder; @@ -44,9 +44,8 @@ import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.util.UniqueIdType; import me.lucko.luckperms.common.util.Uuids; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataType; @@ -55,10 +54,10 @@ import net.luckperms.api.query.Flag; import net.luckperms.api.query.QueryOptions; import net.milkbowl.vault.permission.Permission; - import org.bukkit.entity.Player; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -173,7 +172,7 @@ public boolean userHasPermission(String world, UUID uuid, String permission) { PermissionHolder user = lookupUser(uuid); QueryOptions queryOptions = getQueryOptions(uuid, world); PermissionCache permissionData = user.getCachedData().getPermissionData(queryOptions); - return permissionData.checkPermission(permission, PermissionCheckEvent.Origin.THIRD_PARTY_API).result().asBoolean(); + return permissionData.checkPermission(permission, CheckOrigin.THIRD_PARTY_API).result().asBoolean(); } @Override @@ -185,7 +184,7 @@ public boolean userAddPermission(String world, UUID uuid, String permission) { if (user instanceof Group) { throw new UnsupportedOperationException("Unable to modify the permissions of NPC players"); } - return holderAddPermission(user, permission, world); + return holderAddPermission(user, permission, world, DataType.NORMAL); } @Override @@ -197,7 +196,31 @@ public boolean userRemovePermission(String world, UUID uuid, String permission) if (user instanceof Group) { throw new UnsupportedOperationException("Unable to modify the permissions of NPC players"); } - return holderRemovePermission(user, permission, world); + return holderRemovePermission(user, permission, world, DataType.NORMAL); + } + + @Override + public boolean userAddTransient(String world, UUID uuid, String permission) { + Objects.requireNonNull(uuid, "uuid"); + Objects.requireNonNull(permission, "permission"); + + PermissionHolder user = lookupUser(uuid); + if (user instanceof Group) { + throw new UnsupportedOperationException("Unable to modify the permissions of NPC players"); + } + return holderAddPermission(user, permission, world, DataType.TRANSIENT); + } + + @Override + public boolean userRemoveTransient(String world, UUID uuid, String permission) { + Objects.requireNonNull(uuid, "uuid"); + Objects.requireNonNull(permission, "permission"); + + PermissionHolder user = lookupUser(uuid); + if (user instanceof Group) { + throw new UnsupportedOperationException("Unable to modify the permissions of NPC players"); + } + return holderRemovePermission(user, permission, world, DataType.TRANSIENT); } @Override @@ -209,7 +232,7 @@ public boolean userInGroup(String world, UUID uuid, String group) { QueryOptions queryOptions = getQueryOptions(uuid, world); PermissionCache permissionData = user.getCachedData().getPermissionData(queryOptions); - TristateResult result = permissionData.checkPermission(Inheritance.key(rewriteGroupName(group)), PermissionCheckEvent.Origin.THIRD_PARTY_API); + TristateResult result = permissionData.checkPermission(Inheritance.key(rewriteGroupName(group)), CheckOrigin.THIRD_PARTY_API); return result.processorClass() == DirectProcessor.class && result.result().asBoolean(); } @@ -252,8 +275,11 @@ public String userGetPrimaryGroup(String world, UUID uuid) { } QueryOptions queryOptions = getQueryOptions(uuid, world); - MetaCache metaData = user.getCachedData().getMetaData(queryOptions); - String value = metaData.getPrimaryGroup(MetaCheckEvent.Origin.THIRD_PARTY_API); + MonitoredMetaCache metaData = user.getCachedData().getMetaData(queryOptions); + String value = metaData.getPrimaryGroup(CheckOrigin.THIRD_PARTY_API); + if (value == null) { + return null; + } Group group = getGroup(value); return group != null ? groupName(group) : value; @@ -271,7 +297,7 @@ public boolean groupHasPermission(String world, String name, String permission) QueryOptions queryOptions = getQueryOptions(null, world); PermissionCache permissionData = group.getCachedData().getPermissionData(queryOptions); - return permissionData.checkPermission(permission, PermissionCheckEvent.Origin.THIRD_PARTY_API).result().asBoolean(); + return permissionData.checkPermission(permission, CheckOrigin.THIRD_PARTY_API).result().asBoolean(); } @Override @@ -284,7 +310,7 @@ public boolean groupAddPermission(String world, String name, String permission) return false; } - return holderAddPermission(group, permission, world); + return holderAddPermission(group, permission, world, DataType.NORMAL); } @Override @@ -297,7 +323,7 @@ public boolean groupRemovePermission(String world, String name, String permissio return false; } - return holderRemovePermission(group, permission, world); + return holderRemovePermission(group, permission, world, DataType.NORMAL); } // utility methods for getting user and group instances @@ -328,13 +354,13 @@ private String rewriteGroupName(String name) { // utility method for getting a contexts instance for a given vault lookup. QueryOptions getQueryOptions(@Nullable UUID uuid, @Nullable String world) { - MutableContextSet context; + ContextSet context; Player player = Optional.ofNullable(uuid).flatMap(u -> this.plugin.getBootstrap().getPlayer(u)).orElse(null); if (player != null) { - context = this.plugin.getContextManager().getContext(player).mutableCopy(); + context = this.plugin.getContextManager().getContext(player); } else { - context = this.plugin.getContextManager().getStaticContext().mutableCopy(); + context = this.plugin.getContextManager().getStaticContext(); } String playerWorld = player == null ? null : player.getWorld().getName(); @@ -342,20 +368,26 @@ QueryOptions getQueryOptions(@Nullable UUID uuid, @Nullable String world) { // if world is null, we want to do a lookup in the players current context // if world is not null, we want to do a lookup in that specific world if (world != null && !world.isEmpty() && !world.equalsIgnoreCase(playerWorld)) { + MutableContextSet mutContext = context.mutableCopy(); + context = mutContext; + // remove already accumulated worlds - context.removeAll(DefaultContextKeys.WORLD_KEY); + mutContext.removeAll(DefaultContextKeys.WORLD_KEY); // add the vault world - context.add(DefaultContextKeys.WORLD_KEY, world.toLowerCase()); + mutContext.add(DefaultContextKeys.WORLD_KEY, world.toLowerCase(Locale.ROOT)); } // if we're using a special vault server if (useVaultServer()) { + MutableContextSet mutContext = context instanceof MutableContextSet ? (MutableContextSet) context : context.mutableCopy(); + context = mutContext; + // remove the normal server context from the set - context.remove(DefaultContextKeys.SERVER_KEY, getServer()); + mutContext.remove(DefaultContextKeys.SERVER_KEY, getServer()); // add the vault specific server if (!getVaultServer().equals("global")) { - context.add(DefaultContextKeys.SERVER_KEY, getVaultServer()); + mutContext.add(DefaultContextKeys.SERVER_KEY, getVaultServer()); } } @@ -377,7 +409,7 @@ QueryOptions getQueryOptions(@Nullable UUID uuid, @Nullable String world) { // utility methods for modifying the state of PermissionHolders - private boolean holderAddPermission(PermissionHolder holder, String permission, String world) { + private boolean holderAddPermission(PermissionHolder holder, String permission, String world, DataType type) { Objects.requireNonNull(permission, "permission is null"); Preconditions.checkArgument(!permission.isEmpty(), "permission is an empty string"); @@ -386,13 +418,13 @@ private boolean holderAddPermission(PermissionHolder holder, String permission, .withContext(DefaultContextKeys.WORLD_KEY, world == null ? "global" : world) .build(); - if (holder.setNode(DataType.NORMAL, node, true).wasSuccessful()) { + if (holder.setNode(type, node, true).wasSuccessful()) { return holderSave(holder); } return false; } - private boolean holderRemovePermission(PermissionHolder holder, String permission, String world) { + private boolean holderRemovePermission(PermissionHolder holder, String permission, String world, DataType type) { Objects.requireNonNull(permission, "permission is null"); Preconditions.checkArgument(!permission.isEmpty(), "permission is an empty string"); @@ -401,7 +433,7 @@ private boolean holderRemovePermission(PermissionHolder holder, String permissio .withContext(DefaultContextKeys.WORLD_KEY, world == null ? "global" : world) .build(); - if (holder.unsetNode(DataType.NORMAL, node).wasSuccessful()) { + if (holder.unsetNode(type, node).wasSuccessful()) { return holderSave(holder); } return false; @@ -436,7 +468,12 @@ String getServer() { } String getVaultServer() { - return this.plugin.getConfiguration().get(ConfigKeys.VAULT_SERVER); + LuckPermsConfiguration configuration = this.plugin.getConfiguration(); + if (configuration.get(ConfigKeys.USE_VAULT_SERVER)) { + return configuration.get(ConfigKeys.VAULT_SERVER); + } else { + return configuration.get(ConfigKeys.SERVER); + } } boolean isIncludeGlobal() { @@ -450,4 +487,5 @@ boolean isIgnoreWorld() { private boolean useVaultServer() { return this.plugin.getConfiguration().get(ConfigKeys.USE_VAULT_SERVER); } + } diff --git a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/VaultHookManager.java b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/VaultHookManager.java index 06ec6dd6c..c9507037a 100644 --- a/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/VaultHookManager.java +++ b/bukkit/src/main/java/me/lucko/luckperms/bukkit/vault/VaultHookManager.java @@ -26,10 +26,8 @@ package me.lucko.luckperms.bukkit.vault; import me.lucko.luckperms.bukkit.LPBukkitPlugin; - import net.milkbowl.vault.chat.Chat; import net.milkbowl.vault.permission.Permission; - import org.bukkit.plugin.ServicePriority; import org.bukkit.plugin.ServicesManager; diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index 7fc20df99..30a8bc60b 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 | # @@ -154,15 +154,25 @@ data: #verifyServerCertificate: false # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). # - Change this if you want to use different tables for different servers. table-prefix: 'luckperms_' - # The prefix to use for all LuckPerms collections. Change this if you want to use different - # collections for different servers. The default is no prefix. + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. mongodb-collection-prefix: '' - # MongoDB ClientConnectionURI for use with replica sets and custom connection options - # - See https://docs.mongodb.com/manual/reference/connection-string/ + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ mongodb-connection-uri: '' # Define settings for a "split" storage setup. @@ -231,6 +241,9 @@ watch-files: true # below. # => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be # configured below. +# => nats Uses Nats pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. # => auto Attempts to automatically setup a messaging service using redis or sql. messaging-service: auto @@ -249,10 +262,31 @@ 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 + username: '' password: '' + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel: + enabled: false + master: mymaster + addresses: + - localhost:26379 + username: '' + password: '' + +# Settings for Nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats: + enabled: false + address: localhost + username: '' + password: '' + token: '' # Settings for RabbitMQ. # Port 5672 is used by default; set address to "host:port" if differs @@ -540,6 +574,13 @@ apply-bukkit-attachment-permissions: true # | Extra settings | # # +----------------------------------------------------------------------------------------------+ # +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators: [] + # Allows you to set "aliases" for the worlds sent forward for context calculation. # # - These aliases are provided in addition to the real world name. Applied recursively. @@ -575,7 +616,8 @@ group-weight: # Controls whether server operators should exist at all. # # - When set to 'false', all players will be de-opped, and the /op and /deop commands will be -# disabled. +# disabled. Note that vanilla features like the spawn-protection require an operator on the +# server to work. enable-ops: true # Enables or disables a special permission based system in LuckPerms for controlling OP status. @@ -668,6 +710,12 @@ allow-invalid-usernames: false # - If automation is needed, users should prefer using the LuckPerms API. skip-bulkupdate-confirmation: false +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate: false + # If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. # # - When this happens, the plugin will set their primary group back to default. @@ -680,5 +728,35 @@ update-client-command-list: true register-command-list-data: true # If LuckPerms should attempt to resolve Vanilla command target selectors for LP commands. -# See here for more info: https://minecraft.gamepedia.com/Commands#Target_selectors +# See here for more info: https://minecraft.wiki/w/Target_selectors resolve-command-selectors: false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode: + players: false + console: false + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands: + players: false + console: false diff --git a/bukkit/src/main/resources/luckperms.commodore b/bukkit/src/main/resources/luckperms.commodore index b6693077f..15b030924 100644 --- a/bukkit/src/main/resources/luckperms.commodore +++ b/bukkit/src/main/resources/luckperms.commodore @@ -54,6 +54,12 @@ luckperms { translations { install; } + applyedits { + code brigadier:string single_word; + } + trusteditor { + id brigadier:string single_word; + } creategroup { name brigadier:string single_word { weight brigadier:integer { @@ -62,9 +68,13 @@ luckperms { } } deletegroup { - name brigadier:string single_word; + name brigadier:string single_word { + flags brigadier:string greedy_phrase; + } + } + listgroups { + page brigadier:integer; } - listgroups; createtrack { name brigadier:string single_word; } @@ -533,7 +543,9 @@ luckperms { editor; listmembers { - page brigadier:integer; + page brigadier:integer { + context brigadier:string greedy_phrase; + } } setweight { weight brigadier:integer; @@ -546,7 +558,9 @@ luckperms { context brigadier:string greedy_phrase; } rename { - newname brigadier:string single_word; + newname brigadier:string single_word { + flags brigadier:string greedy_phrase; + } } clone { newname brigadier:string single_word; diff --git a/bungee/build.gradle b/bungee/build.gradle index 685657a5a..bac4f56dc 100644 --- a/bungee/build.gradle +++ b/bungee/build.gradle @@ -1,13 +1,15 @@ plugins { - id 'com.github.johnrengelman.shadow' version '2.0.4' + alias(libs.plugins.shadow) } dependencies { - compile project(':common') + implementation project(':common') compileOnly project(':common:loader-utils') - compileOnly 'net.md-5:bungeecord-api:1.15-SNAPSHOT' - compileOnly('me.lucko:adventure-platform-bungeecord:4.7.0') { + compileOnly('net.md-5:bungeecord-api:1.19-R0.1-SNAPSHOT') { + exclude(module: 'bungeecord-protocol') + } + compileOnly('net.kyori:adventure-platform-bungeecord:4.4.0') { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') exclude(module: 'adventure-nbt') @@ -16,7 +18,7 @@ dependencies { } shadowJar { - archiveName = 'luckperms-bungee.jarinjar' + archiveFileName = 'luckperms-bungee.jarinjar' dependencies { include(dependency('me.lucko.luckperms:.*')) @@ -36,6 +38,7 @@ shadowJar { relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' diff --git a/bungee/loader/build.gradle b/bungee/loader/build.gradle index 80f237a8e..71ebfe107 100644 --- a/bungee/loader/build.gradle +++ b/bungee/loader/build.gradle @@ -1,23 +1,24 @@ plugins { - id 'com.github.johnrengelman.shadow' + alias(libs.plugins.shadow) } dependencies { - compileOnly 'net.md-5:bungeecord-api:1.15-SNAPSHOT' + compileOnly('net.md-5:bungeecord-api:1.19-R0.1-SNAPSHOT') { + exclude(module: 'bungeecord-protocol') + } - compile project(':api') - compile project(':common:loader-utils') + implementation project(':api') + implementation project(':common:loader-utils') } processResources { - from(sourceSets.main.resources.srcDirs) { + filesMatching('plugin.yml') { expand 'pluginVersion': project.ext.fullVersion - include 'plugin.yml' } } shadowJar { - archiveName = "LuckPerms-Bungee-${project.ext.fullVersion}.jar" + archiveFileName = "LuckPerms-Bungee-${project.ext.fullVersion}.jar" from { project(':bungee').tasks.shadowJar.archiveFile @@ -26,4 +27,4 @@ shadowJar { artifacts { archives shadowJar -} \ No newline at end of file +} diff --git a/bungee/loader/src/main/java/me/lucko/luckperms/bungee/loader/BungeeLoaderPlugin.java b/bungee/loader/src/main/java/me/lucko/luckperms/bungee/loader/BungeeLoaderPlugin.java index 48167204d..2f3715efa 100644 --- a/bungee/loader/src/main/java/me/lucko/luckperms/bungee/loader/BungeeLoaderPlugin.java +++ b/bungee/loader/src/main/java/me/lucko/luckperms/bungee/loader/BungeeLoaderPlugin.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.loader.JarInJarClassLoader; import me.lucko.luckperms.common.loader.LoaderBootstrap; - import net.md_5.bungee.api.plugin.Plugin; public class BungeeLoaderPlugin extends Plugin { diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeCommandExecutor.java b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeCommandExecutor.java index 0b2e555e2..607978a28 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeCommandExecutor.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeCommandExecutor.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.command.CommandManager; import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; import me.lucko.luckperms.common.sender.Sender; - import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.plugin.Command; diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeConfigAdapter.java b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeConfigAdapter.java index edfae1728..43230f956 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeConfigAdapter.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeConfigAdapter.java @@ -27,14 +27,12 @@ import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.md_5.bungee.config.Configuration; import net.md_5.bungee.config.ConfigurationProvider; import net.md_5.bungee.config.YamlConfiguration; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -80,16 +78,6 @@ public List getStringList(String path, List def) { return Optional.ofNullable(this.configuration.getStringList(path)).orElse(def); } - @Override - public List getKeys(String path, List def) { - Configuration section = this.configuration.getSection(path); - if (section == null) { - return def; - } - - return Optional.of((List) new ArrayList<>(section.getKeys())).orElse(def); - } - @Override public Map getStringMap(String path, Map def) { Map map = new HashMap<>(); diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeEventBus.java b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeEventBus.java index e7ba49caf..91b983e33 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeEventBus.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeEventBus.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.event.AbstractEventBus; - import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Plugin; diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeSchedulerAdapter.java b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeSchedulerAdapter.java index 573bfd886..4e7ee92f9 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeSchedulerAdapter.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeSchedulerAdapter.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; import me.lucko.luckperms.common.util.Iterators; - import net.md_5.bungee.api.scheduler.ScheduledTask; import java.util.Collections; @@ -53,11 +52,6 @@ public Executor async() { return this.executor; } - @Override - public Executor sync() { - return this.executor; - } - @Override public SchedulerTask asyncLater(Runnable task, long delay, TimeUnit unit) { ScheduledTask t = this.bootstrap.getProxy().getScheduler().schedule(this.bootstrap.getLoader(), task, delay, unit); diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeSenderFactory.java b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeSenderFactory.java index 1cef0fe74..0aa68df21 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeSenderFactory.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/BungeeSenderFactory.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.bungee.event.TristateCheckEvent; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.sender.SenderFactory; - import net.kyori.adventure.platform.bungeecord.BungeeAudiences; import net.kyori.adventure.text.Component; import net.luckperms.api.util.Tristate; diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeeBootstrap.java b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeeBootstrap.java index 3174c80a0..4a932bb19 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeeBootstrap.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeeBootstrap.java @@ -27,19 +27,18 @@ import me.lucko.luckperms.bungee.util.RedisBungeeUtil; import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; import me.lucko.luckperms.common.plugin.logging.JavaPluginLogger; import me.lucko.luckperms.common.plugin.logging.PluginLogger; import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; - import net.luckperms.api.platform.Platform; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.PluginDescription; - import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.reflect.Field; @@ -56,7 +55,7 @@ /** * Bootstrap plugin for LuckPerms running on BungeeCord. */ -public class LPBungeeBootstrap implements LuckPermsBootstrap, LoaderBootstrap { +public class LPBungeeBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { private final Plugin loader; /** @@ -101,6 +100,7 @@ public LPBungeeBootstrap(Plugin loader) { // provide adapters + @Override public Plugin getLoader() { return this.loader; } @@ -302,7 +302,7 @@ private static boolean checkIncompatibleVersion() { try { Class.forName("com.google.gson.internal.bind.TreeTypeAdapter"); return false; - } catch (ClassNotFoundException e) { + } catch (Exception e) { return true; } } diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java index 171de9ad6..ec6066b42 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/LPBungeePlugin.java @@ -47,9 +47,6 @@ import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.tasks.CacheHousekeepingTask; -import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; - import net.luckperms.api.LuckPerms; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.query.QueryOptions; @@ -57,7 +54,6 @@ import java.util.Optional; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.stream.Stream; /** @@ -170,12 +166,6 @@ protected void registerApiOnPlatform(LuckPerms api) { // BungeeCord doesn't have a services manager } - @Override - protected void registerHousekeepingTasks() { - this.bootstrap.getScheduler().asyncRepeating(new ExpireTemporaryTask(this), 3, TimeUnit.SECONDS); - this.bootstrap.getScheduler().asyncRepeating(new CacheHousekeepingTask(this), 2, TimeUnit.MINUTES); - } - @Override protected void performFinalSetup() { diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/calculator/BungeeCalculatorFactory.java b/bungee/src/main/java/me/lucko/luckperms/bungee/calculator/BungeeCalculatorFactory.java index f52a00f39..def17b1c1 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/calculator/BungeeCalculatorFactory.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/calculator/BungeeCalculatorFactory.java @@ -29,17 +29,19 @@ import me.lucko.luckperms.common.cacheddata.CacheMetadata; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorMonitored; import me.lucko.luckperms.common.calculator.processor.DirectProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import me.lucko.luckperms.common.calculator.processor.RegexProcessor; import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; import me.lucko.luckperms.common.config.ConfigKeys; - +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class BungeeCalculatorFactory implements CalculatorFactory { private final LPBungeePlugin plugin; @@ -49,23 +51,23 @@ public BungeeCalculatorFactory(LPBungeePlugin plugin) { } @Override - public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { + public PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata) { List processors = new ArrayList<>(4); - processors.add(new DirectProcessor()); + processors.add(new DirectProcessor(sourceMap)); if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { - processors.add(new RegexProcessor()); + processors.add(new RegexProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { - processors.add(new WildcardProcessor()); + processors.add(new WildcardProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { - processors.add(new SpongeWildcardProcessor()); + processors.add(new SpongeWildcardProcessor(sourceMap)); } - return new PermissionCalculator(this.plugin, metadata, processors); + return new PermissionCalculatorMonitored(this.plugin, metadata, processors); } } diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/context/BungeeContextManager.java b/bungee/src/main/java/me/lucko/luckperms/bungee/context/BungeeContextManager.java index 3050dbdc0..f1bfc25f5 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/context/BungeeContextManager.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/context/BungeeContextManager.java @@ -25,26 +25,13 @@ package me.lucko.luckperms.bungee.context; -import com.github.benmanes.caffeine.cache.LoadingCache; - import me.lucko.luckperms.bungee.LPBungeePlugin; -import me.lucko.luckperms.common.context.ContextManager; -import me.lucko.luckperms.common.context.QueryOptionsSupplier; -import me.lucko.luckperms.common.util.CaffeineFactory; - -import net.luckperms.api.context.ImmutableContextSet; -import net.luckperms.api.query.QueryOptions; +import me.lucko.luckperms.common.context.manager.SimpleContextManager; import net.md_5.bungee.api.connection.ProxiedPlayer; import java.util.UUID; -import java.util.concurrent.TimeUnit; - -public class BungeeContextManager extends ContextManager { - - private final LoadingCache contextsCache = CaffeineFactory.newBuilder() - .expireAfterWrite(50, TimeUnit.MILLISECONDS) - .build(this::calculate); +public class BungeeContextManager extends SimpleContextManager { public BungeeContextManager(LPBungeePlugin plugin) { super(plugin, ProxiedPlayer.class, ProxiedPlayer.class); } @@ -53,48 +40,4 @@ public BungeeContextManager(LPBungeePlugin plugin) { public UUID getUniqueId(ProxiedPlayer player) { return player.getUniqueId(); } - - @Override - public QueryOptionsSupplier getCacheFor(ProxiedPlayer subject) { - if (subject == null) { - throw new NullPointerException("subject"); - } - - return new InlineQueryOptionsSupplier(subject, this.contextsCache); - } - - @Override - public ImmutableContextSet getContext(ProxiedPlayer subject) { - return getQueryOptions(subject).context(); - } - - @Override - public QueryOptions getQueryOptions(ProxiedPlayer subject) { - return this.contextsCache.get(subject); - } - - @Override - protected void invalidateCache(ProxiedPlayer subject) { - this.contextsCache.invalidate(subject); - } - - @Override - public QueryOptions formQueryOptions(ProxiedPlayer subject, ImmutableContextSet contextSet) { - return formQueryOptions(contextSet); - } - - private static final class InlineQueryOptionsSupplier implements QueryOptionsSupplier { - private final ProxiedPlayer key; - private final LoadingCache cache; - - private InlineQueryOptionsSupplier(ProxiedPlayer key, LoadingCache cache) { - this.key = key; - this.cache = cache; - } - - @Override - public QueryOptions getQueryOptions() { - return this.cache.get(this.key); - } - } } diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/context/BungeePlayerCalculator.java b/bungee/src/main/java/me/lucko/luckperms/bungee/context/BungeePlayerCalculator.java index 278a911b2..bfb8a7009 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/context/BungeePlayerCalculator.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/context/BungeePlayerCalculator.java @@ -27,8 +27,7 @@ import me.lucko.luckperms.bungee.LPBungeePlugin; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; - +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import net.luckperms.api.context.ContextCalculator; import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.ContextSet; @@ -41,7 +40,6 @@ import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventPriority; - import org.checkerframework.checker.nullness.qual.NonNull; public class BungeePlayerCalculator implements ContextCalculator, Listener { diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/context/RedisBungeeCalculator.java b/bungee/src/main/java/me/lucko/luckperms/bungee/context/RedisBungeeCalculator.java index 682d6dd78..c26fa4a60 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/context/RedisBungeeCalculator.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/context/RedisBungeeCalculator.java @@ -27,14 +27,11 @@ import com.imaginarycode.minecraft.redisbungee.RedisBungee; import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI; - -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; - +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.context.StaticContextCalculator; - import org.checkerframework.checker.nullness.qual.NonNull; public class RedisBungeeCalculator implements StaticContextCalculator { diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeeConnectionListener.java b/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeeConnectionListener.java index 413512975..1b1da0910 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeeConnectionListener.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeeConnectionListener.java @@ -31,7 +31,6 @@ import me.lucko.luckperms.common.locale.TranslationManager; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; - import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; import net.md_5.bungee.api.connection.PendingConnection; diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeePermissionCheckListener.java b/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeePermissionCheckListener.java index 18ddad379..ac512c865 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeePermissionCheckListener.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/listeners/BungeePermissionCheckListener.java @@ -27,13 +27,12 @@ import me.lucko.luckperms.bungee.LPBungeePlugin; import me.lucko.luckperms.bungee.event.TristateCheckEvent; -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent.Origin; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; import net.md_5.bungee.api.connection.ProxiedPlayer; @@ -72,7 +71,7 @@ public void onPlayerPermissionCheck(PermissionCheckEvent e) { } QueryOptions queryOptions = this.plugin.getContextManager().getQueryOptions(player); - Tristate result = user.getCachedData().getPermissionData(queryOptions).checkPermission(e.getPermission(), Origin.PLATFORM_PERMISSION_CHECK).result(); + Tristate result = user.getCachedData().getPermissionData(queryOptions).checkPermission(e.getPermission(), CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); if (result == Tristate.UNDEFINED && this.plugin.getConfiguration().get(ConfigKeys.APPLY_BUNGEE_CONFIG_PERMISSIONS)) { return; // just use the result provided by the proxy when the event was created } @@ -101,7 +100,7 @@ public void onPlayerTristateCheck(TristateCheckEvent e) { } QueryOptions queryOptions = this.plugin.getContextManager().getQueryOptions(player); - Tristate result = user.getCachedData().getPermissionData(queryOptions).checkPermission(e.getPermission(), Origin.PLATFORM_LOOKUP_CHECK).result(); + Tristate result = user.getCachedData().getPermissionData(queryOptions).checkPermission(e.getPermission(), CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET).result(); if (result == Tristate.UNDEFINED && this.plugin.getConfiguration().get(ConfigKeys.APPLY_BUNGEE_CONFIG_PERMISSIONS)) { return; // just use the result provided by the proxy when the event was created } @@ -122,7 +121,7 @@ public void onOtherPermissionCheck(PermissionCheckEvent e) { Tristate result = Tristate.of(e.hasPermission()); VerboseCheckTarget target = VerboseCheckTarget.internal(e.getSender().getName()); - this.plugin.getVerboseHandler().offerPermissionCheckEvent(Origin.PLATFORM_PERMISSION_CHECK, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.of(result)); + this.plugin.getVerboseHandler().offerPermissionCheckEvent(CheckOrigin.PLATFORM_API_HAS_PERMISSION, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.forMonitoredResult(result)); this.plugin.getPermissionRegistry().offer(permission); } @@ -139,7 +138,7 @@ public void onOtherTristateCheck(TristateCheckEvent e) { Tristate result = e.getResult(); VerboseCheckTarget target = VerboseCheckTarget.internal(e.getSender().getName()); - this.plugin.getVerboseHandler().offerPermissionCheckEvent(Origin.PLATFORM_LOOKUP_CHECK, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.of(result)); + this.plugin.getVerboseHandler().offerPermissionCheckEvent(CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.forMonitoredResult(result)); this.plugin.getPermissionRegistry().offer(permission); } } diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/BungeeMessagingFactory.java b/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/BungeeMessagingFactory.java index 4d53bc956..cd7aa2efe 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/BungeeMessagingFactory.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/BungeeMessagingFactory.java @@ -29,11 +29,9 @@ import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; import me.lucko.luckperms.common.messaging.MessagingFactory; - import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; - import org.checkerframework.checker.nullness.qual.NonNull; public class BungeeMessagingFactory extends MessagingFactory { diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/PluginMessageMessenger.java b/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/PluginMessageMessenger.java index 6eee535b0..89ac9aa05 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/PluginMessageMessenger.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/PluginMessageMessenger.java @@ -25,15 +25,10 @@ package me.lucko.luckperms.bungee.messaging; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; - import me.lucko.luckperms.bungee.LPBungeePlugin; - +import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; -import net.luckperms.api.messenger.message.OutgoingMessage; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; @@ -41,20 +36,15 @@ import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; -import org.checkerframework.checker.nullness.qual.NonNull; - /** * An implementation of {@link Messenger} using the plugin messaging channels. */ -public class PluginMessageMessenger implements Messenger, Listener { - private static final String CHANNEL = "luckperms:update"; - +public class PluginMessageMessenger extends AbstractPluginMessageMessenger implements Listener { private final LPBungeePlugin plugin; - private final IncomingMessageConsumer consumer; public PluginMessageMessenger(LPBungeePlugin plugin, IncomingMessageConsumer consumer) { + super(consumer); this.plugin = plugin; - this.consumer = consumer; } public void init() { @@ -70,21 +60,13 @@ public void close() { proxy.getPluginManager().unregisterListener(this); } - private void dispatchMessage(byte[] message) { + @Override + protected void sendOutgoingMessage(byte[] buf) { for (ServerInfo server : this.plugin.getBootstrap().getProxy().getServers().values()) { - server.sendData(CHANNEL, message, false); + server.sendData(CHANNEL, buf, false); } } - @Override - public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF(outgoingMessage.asEncodedString()); - - byte[] message = out.toByteArray(); - dispatchMessage(message); - } - @EventHandler public void onPluginMessage(PluginMessageEvent e) { if (!e.getTag().equals(CHANNEL)) { @@ -97,14 +79,11 @@ public void onPluginMessage(PluginMessageEvent e) { return; } - byte[] data = e.getData(); - - ByteArrayDataInput in = ByteStreams.newDataInput(data); - String msg = in.readUTF(); + byte[] buf = e.getData(); - if (this.consumer.consumeIncomingMessageAsString(msg)) { + if (handleIncomingMessage(buf)) { // Forward to other servers - this.plugin.getBootstrap().getScheduler().executeAsync(() -> dispatchMessage(data)); + this.plugin.getBootstrap().getScheduler().executeAsync(() -> sendOutgoingMessage(buf)); } } } diff --git a/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/RedisBungeeMessenger.java b/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/RedisBungeeMessenger.java index 5360a7cce..409296648 100644 --- a/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/RedisBungeeMessenger.java +++ b/bungee/src/main/java/me/lucko/luckperms/bungee/messaging/RedisBungeeMessenger.java @@ -28,15 +28,12 @@ import com.imaginarycode.minecraft.redisbungee.RedisBungee; import com.imaginarycode.minecraft.redisbungee.RedisBungeeAPI; import com.imaginarycode.minecraft.redisbungee.events.PubSubMessageEvent; - import me.lucko.luckperms.bungee.LPBungeePlugin; - import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.message.OutgoingMessage; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.event.EventHandler; - import org.checkerframework.checker.nullness.qual.NonNull; /** diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index 27d5655f5..9b6a395ff 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 | # @@ -151,15 +151,25 @@ data: #verifyServerCertificate: false # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). # - Change this if you want to use different tables for different servers. table-prefix: 'luckperms_' - # The prefix to use for all LuckPerms collections. Change this if you want to use different - # collections for different servers. The default is no prefix. + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. mongodb-collection-prefix: '' - # MongoDB ClientConnectionURI for use with replica sets and custom connection options - # - See https://docs.mongodb.com/manual/reference/connection-string/ + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ mongodb-connection-uri: '' # Define settings for a "split" storage setup. @@ -229,6 +239,9 @@ watch-files: true # the RedisBungee plugin installed. # => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be # configured below. +# => nats Uses Nats pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. # => auto Attempts to automatically setup a messaging service using redis or sql. messaging-service: auto @@ -247,10 +260,31 @@ 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 + username: '' password: '' + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel: + enabled: false + master: mymaster + addresses: + - localhost:26379 + username: '' + password: '' + +# Settings for Nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats: + enabled: false + address: localhost + username: '' + password: '' + token: '' # Settings for RabbitMQ. # Port 5672 is used by default; set address to "host:port" if differs @@ -520,6 +554,13 @@ apply-bungee-config-permissions: false # | Extra settings | # # +----------------------------------------------------------------------------------------------+ # +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators: [] + # Allows you to set "aliases" for the worlds sent forward for context calculation. # # - These aliases are provided in addition to the real world name. Applied recursively. @@ -571,7 +612,43 @@ allow-invalid-usernames: false # - If automation is needed, users should prefer using the LuckPerms API. skip-bulkupdate-confirmation: false +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate: false + # If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. # # - When this happens, the plugin will set their primary group back to default. prevent-primary-group-removal: false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode: + players: false + console: false + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands: + players: false + console: false diff --git a/common/build.gradle b/common/build.gradle index 14f2579d6..ab2cc373f 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,73 +1,117 @@ +plugins { + id("java-library") + id("jacoco") + alias(libs.plugins.blossom) +} + test { useJUnitPlatform { - excludeTags 'dependency_checksum' + if (!project.hasProperty('dockerTests')) { + excludeTags 'docker' + } + } +} + +jacocoTestReport { + dependsOn test +} + +sourceSets { + main { + blossom { + javaSources { + property 'version', project.ext.fullVersion + } + } } } dependencies { - testCompile 'org.junit.jupiter:junit-jupiter-api:5.7.0' - testCompile 'org.junit.jupiter:junit-jupiter-engine:5.7.0' + testImplementation 'org.slf4j:slf4j-simple:1.7.36' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - compile project(':api') - compile 'org.checkerframework:checker-qual:3.12.0' + testImplementation 'org.testcontainers:testcontainers-junit-jupiter:2.0.2' + testImplementation 'org.mockito:mockito-core:5.18.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0' + + testImplementation 'com.h2database:h2:2.1.214' + testImplementation 'org.mongodb:mongodb-driver-legacy:5.5.0' + testImplementation 'org.spongepowered:configurate-yaml:3.7.3' + testImplementation 'org.spongepowered:configurate-hocon:3.7.3' + testImplementation 'me.lucko.configurate:configurate-toml:3.7' + testImplementation 'net.luckperms:rest-api-java-client:0.1' + + api project(':api') + api 'org.checkerframework:checker-qual:3.12.0' compileOnly project(':common:loader-utils') compileOnly 'org.slf4j:slf4j-api:1.7.30' compileOnly 'org.apache.logging.log4j:log4j-api:2.14.0' - compile('net.kyori:adventure-api:4.7.0') { + api('net.kyori:adventure-api:4.21.0') { exclude(module: 'adventure-bom') exclude(module: 'checker-qual') exclude(module: 'annotations') } - compile('net.kyori:adventure-text-serializer-gson:4.7.0') { + api('net.kyori:adventure-text-serializer-gson:4.21.0') { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') exclude(module: 'gson') } - compile('net.kyori:adventure-text-serializer-legacy:4.7.0') { + api('net.kyori:adventure-text-serializer-legacy:4.21.0') { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') } - compile('net.kyori:adventure-text-serializer-plain:4.7.0') { + api('net.kyori:adventure-text-serializer-plain:4.21.0') { exclude(module: 'adventure-bom') exclude(module: 'adventure-api') } - compile('net.kyori:event-api:3.0.0') { + api("net.kyori:adventure-text-minimessage:4.21.0") { + exclude(module: 'adventure-bom') + exclude(module: 'adventure-api') + } + + api('net.kyori:event-api:3.0.0') { exclude(module: 'checker-qual') exclude(module: 'guava') } - compile 'com.google.code.gson:gson:2.7' - compile 'com.google.guava:guava:19.0' + api 'com.google.code.gson:gson:2.7' + api 'com.google.guava:guava:19.0' + + api 'com.github.ben-manes.caffeine:caffeine:3.2.0' + api 'com.squareup.okhttp3:okhttp:3.14.9' + api 'com.squareup.okio:okio:1.17.6' + api 'net.bytebuddy:byte-buddy:1.15.11' - compile 'com.github.ben-manes.caffeine:caffeine:2.9.0' - compile 'com.squareup.okhttp3:okhttp:3.14.9' - compile 'com.squareup.okio:okio:1.17.5' - compile 'net.bytebuddy:byte-buddy:1.10.22' - compile('org.spongepowered:configurate-core:3.7.2') { + api('org.spongepowered:configurate-core:3.7.3') { transitive = false } - compile('org.spongepowered:configurate-yaml:3.7.2') { + api('org.spongepowered:configurate-yaml:3.7.3') { transitive = false } - compile('org.spongepowered:configurate-gson:3.7.2') { + api('org.spongepowered:configurate-gson:3.7.3') { transitive = false } - compile ('org.spongepowered:configurate-hocon:3.7.2') { + api('org.spongepowered:configurate-hocon:3.7.3') { transitive = false } - compile('me.lucko.configurate:configurate-toml:3.7') { + api('me.lucko.configurate:configurate-toml:3.7') { transitive = false } - compile 'com.zaxxer:HikariCP:4.0.3' - compile 'redis.clients:jedis:3.5.2' - compile 'com.rabbitmq:amqp-client:5.12.0' - compile 'org.mongodb:mongo-java-driver:3.12.8' - compile 'org.yaml:snakeyaml:1.28' + compileOnly 'com.zaxxer:HikariCP:6.3.0' + compileOnly 'redis.clients:jedis:5.2.0' + compileOnly 'io.nats:jnats:2.21.1' + compileOnly 'com.rabbitmq:amqp-client:5.25.0' + compileOnly 'org.mongodb:mongodb-driver-legacy:5.5.0' + compileOnly 'org.postgresql:postgresql:42.7.6' + compileOnly 'org.yaml:snakeyaml:1.33' + compileOnly 'net.luckperms:rest-api-java-client:0.1' } diff --git a/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/JarInJarClassLoader.java b/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/JarInJarClassLoader.java index 381952a3e..2d2f6c8d0 100644 --- a/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/JarInJarClassLoader.java +++ b/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/JarInJarClassLoader.java @@ -33,7 +33,9 @@ import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.List; /** * Classloader that can load a jar from within another jar file. @@ -49,6 +51,11 @@ public class JarInJarClassLoader extends URLClassLoader { ClassLoader.registerAsParallelCapable(); } + /** + * A list of package prefixes to attempt to load from the URLs first, before delegating to the parent classloader. + */ + private List priorityPackagePrefixes = null; + /** * Creates a new jar-in-jar class loader. * @@ -64,6 +71,24 @@ public void addJarToClasspath(URL url) { addURL(url); } + public void setPriorityPackagePrefixes(List priorityPackagePrefixes) { + this.priorityPackagePrefixes = priorityPackagePrefixes; + } + + public void deleteJarResource() { + URL[] urls = getURLs(); + if (urls.length == 0) { + return; + } + + try { + Path path = Paths.get(urls[0].toURI()); + Files.deleteIfExists(path); + } catch (Exception e) { + // ignore + } + } + /** * Creates a new plugin instance. * @@ -137,4 +162,41 @@ private static URL extractJar(ClassLoader loaderClassLoader, String jarResourceP } } + private boolean shouldLoadFromUrlsFirst(String name) { + if (this.priorityPackagePrefixes == null) { + return false; + } + + for (String prefix : this.priorityPackagePrefixes) { + if (name.startsWith(prefix)) { + return true; + } + } + return false; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (!shouldLoadFromUrlsFirst(name)) { + return super.loadClass(name, resolve); + } + + synchronized (getClassLoadingLock(name)) { + Class clazz = findLoadedClass(name); + if (clazz != null) { + return clazz; + } + + try { + clazz = findClass(name); + } catch (ClassNotFoundException e) { + clazz = super.loadClass(name, false); + } + if (resolve) { + resolveClass(clazz); + } + return clazz; + } + } + } diff --git a/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/LoaderBootstrap.java b/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/LoaderBootstrap.java index 875a2b153..910a51f01 100644 --- a/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/LoaderBootstrap.java +++ b/common/loader-utils/src/main/java/me/lucko/luckperms/common/loader/LoaderBootstrap.java @@ -32,8 +32,8 @@ public interface LoaderBootstrap { void onLoad(); - void onEnable(); + default void onEnable() {} - void onDisable(); + default void onDisable() {} } diff --git a/common/minecraft/build.gradle b/common/minecraft/build.gradle new file mode 100644 index 000000000..69d6eec29 --- /dev/null +++ b/common/minecraft/build.gradle @@ -0,0 +1,17 @@ +plugins { + alias(libs.plugins.loom) +} + +tasks.withType(JavaCompile).configureEach { + options.release = 25 +} + +repositories { + maven { url 'https://maven.fabricmc.net/' } +} + +dependencies { + // https://modmuss50.me/fabric.html + minecraft 'com.mojang:minecraft:26.2' + implementation project(':common') +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftLuckPermsBootstrap.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftLuckPermsBootstrap.java new file mode 100644 index 000000000..9e390123c --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftLuckPermsBootstrap.java @@ -0,0 +1,106 @@ +/* + * 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.minecraft; + +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.NameAndId; +import net.minecraft.server.players.PlayerList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public abstract class MinecraftLuckPermsBootstrap implements LuckPermsBootstrap { + + public abstract Optional getServer(); + + @Override + public abstract MinecraftSchedulerAdapter getScheduler(); + + @Override + public final Optional getPlayer(UUID uniqueId) { + return getServer().map(MinecraftServer::getPlayerList).map(playerList -> playerList.getPlayer(uniqueId)); + } + + @Override + public final Optional lookupUniqueId(String username) { + return getServer().map(server -> server.services().nameToIdCache()) + .flatMap(resolver -> resolver.get(username)).map(NameAndId::id); + } + + @Override + public final Optional lookupUsername(UUID uniqueId) { + return getServer().map(server -> server.services().nameToIdCache()) + .flatMap(resolver -> resolver.get(uniqueId)).map(NameAndId::name); + } + + @Override + public final int getPlayerCount() { + return getServer().map(MinecraftServer::getPlayerCount).orElse(0); + } + + @Override + public final Collection getPlayerList() { + return getServer().map(MinecraftServer::getPlayerList) + .map(PlayerList::getPlayers) + .map(players -> { + List list = new ArrayList<>(players.size()); + for (ServerPlayer player : players) { + list.add(player.getGameProfile().name()); + } + return list; + }) + .orElse(Collections.emptyList()); + } + + @Override + public final Collection getOnlinePlayers() { + return getServer().map(MinecraftServer::getPlayerList) + .map(PlayerList::getPlayers) + .map(players -> { + List list = new ArrayList<>(players.size()); + for (ServerPlayer player : players) { + list.add(player.getGameProfile().id()); + } + return list; + }) + .orElse(Collections.emptyList()); + } + + @Override + public final boolean isPlayerOnline(UUID uniqueId) { + return getServer().map(MinecraftServer::getPlayerList) + .map(s -> s.getPlayer(uniqueId) != null) + .orElse(false); + } + +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftLuckPermsPlugin.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftLuckPermsPlugin.java new file mode 100644 index 000000000..fb5ff931b --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftLuckPermsPlugin.java @@ -0,0 +1,127 @@ +/* + * 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.minecraft; + +import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.minecraft.calculator.MinecraftCalculatorFactory; +import me.lucko.luckperms.common.minecraft.context.MinecraftContextManager; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.model.manager.track.StandardTrackManager; +import me.lucko.luckperms.common.model.manager.user.StandardUserManager; +import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; +import me.lucko.luckperms.common.sender.DummyConsoleSender; +import me.lucko.luckperms.common.sender.Sender; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.PlayerList; + +import java.util.Optional; +import java.util.stream.Stream; + +public abstract class MinecraftLuckPermsPlugin

    , B extends MinecraftLuckPermsBootstrap> extends AbstractLuckPermsPlugin { + protected final B bootstrap; + + private StandardUserManager userManager; + private StandardGroupManager groupManager; + private StandardTrackManager trackManager; + + protected MinecraftLuckPermsPlugin(B bootstrap) { + this.bootstrap = bootstrap; + } + + @Override + public final B getBootstrap() { + return this.bootstrap; + } + + @Override + public abstract MinecraftContextManager getContextManager(); + + public abstract MinecraftSenderFactory

    getSenderFactory(); + + @Override + protected void setupManagers() { + this.userManager = new StandardUserManager(this); + this.groupManager = new StandardGroupManager(this); + this.trackManager = new StandardTrackManager(this); + } + + @Override + protected CalculatorFactory provideCalculatorFactory() { + return new MinecraftCalculatorFactory(this); + } + + @Override + public final Optional getQueryOptionsForUser(User user) { + return this.bootstrap.getPlayer(user.getUniqueId()).map(player -> getContextManager().getQueryOptions(player)); + } + + @Override + public final Stream getOnlineSenders() { + return Stream.concat( + Stream.of(getConsoleSender()), + this.bootstrap.getServer() + .map(MinecraftServer::getPlayerList) + .map(PlayerList::getPlayers) + .stream() + .flatMap(players -> players.stream() + .map(player -> getSenderFactory().wrap(player.createCommandSourceStack())) + ) + ); + } + + @Override + public final Sender getConsoleSender() { + return this.bootstrap.getServer() + .map(server -> getSenderFactory().wrap(server.createCommandSourceStack())) + .orElseGet(() -> new DummyConsoleSender(this) { + @Override + public void sendMessage(Component message) { + MinecraftLuckPermsPlugin.this.bootstrap.getPluginLogger().info(PlainTextComponentSerializer.plainText().serialize(TranslationManager.render(message))); + } + }); + } + + @Override + public StandardUserManager getUserManager() { + return this.userManager; + } + + @Override + public StandardGroupManager getGroupManager() { + return this.groupManager; + } + + @Override + public StandardTrackManager getTrackManager() { + return this.trackManager; + } + +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftSchedulerAdapter.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftSchedulerAdapter.java new file mode 100644 index 000000000..7082ca457 --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftSchedulerAdapter.java @@ -0,0 +1,53 @@ +/* + * 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.minecraft; + +import me.lucko.luckperms.common.plugin.scheduler.JavaSchedulerAdapter; +import me.lucko.luckperms.common.sender.Sender; + +import java.util.concurrent.Executor; + +public class MinecraftSchedulerAdapter extends JavaSchedulerAdapter { + private final Executor syncExecutor; + + public MinecraftSchedulerAdapter(MinecraftLuckPermsBootstrap bootstrap) { + super(bootstrap); + this.syncExecutor = r -> bootstrap.getServer().orElseThrow(() -> new IllegalStateException("Server not ready")).execute(r); + } + + public Executor sync() { + return this.syncExecutor; + } + + public void executeSync(Runnable task) { + this.syncExecutor.execute(task); + } + + @Override + public void executeSync(Sender ctx, Runnable task) { + this.syncExecutor.execute(task); + } +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftSenderFactory.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftSenderFactory.java new file mode 100644 index 000000000..f98fbe2c5 --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/MinecraftSenderFactory.java @@ -0,0 +1,109 @@ +/* + * 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.minecraft; + +import com.mojang.serialization.JsonOps; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.sender.SenderFactory; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.rcon.RconConsoleSource; + +import java.util.Locale; +import java.util.UUID; + +public abstract class MinecraftSenderFactory

    extends SenderFactory { + private final P plugin; + + public MinecraftSenderFactory(P plugin) { + super(plugin); + this.plugin = plugin; + } + + @Override + protected P getPlugin() { + return this.plugin; + } + + protected abstract CommandSource getSource(CommandSourceStack sender); + + @Override + protected UUID getUniqueId(CommandSourceStack commandSource) { + if (commandSource.getEntity() != null) { + return commandSource.getEntity().getUUID(); + } + return Sender.CONSOLE_UUID; + } + + @Override + protected String getName(CommandSourceStack commandSource) { + String name = commandSource.getTextName(); + if (commandSource.getEntity() != null && name.equals("Server")) { + return Sender.CONSOLE_NAME; + } + return name; + } + + @Override + protected void sendMessage(CommandSourceStack sender, Component message) { + Locale locale = sender.getEntity() instanceof ServerPlayer player + ? TranslationManager.parseLocale(player.clientInformation().language()) + : null; + sender.sendSuccess(() -> toNativeText(TranslationManager.render(message, locale)), false); + } + + @Override + protected boolean hasPermission(CommandSourceStack commandSource, String node) { + return getPermissionValue(commandSource, node).asBoolean(); + } + + @Override + protected void performCommand(CommandSourceStack sender, String command) { + sender.getServer().getCommands().performPrefixedCommand(sender, command); + } + + @Override + protected boolean isConsole(CommandSourceStack sender) { + CommandSource output = getSource(sender); + 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) { + return ComponentSerialization.CODEC.decode( + RegistryAccess.EMPTY.createSerializationContext(JsonOps.INSTANCE), + GsonComponentSerializer.gson().serializeToTree(component) + ).getOrThrow(IllegalArgumentException::new).getFirst(); + } +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/calculator/MinecraftCalculatorFactory.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/calculator/MinecraftCalculatorFactory.java new file mode 100644 index 000000000..dff9507d0 --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/calculator/MinecraftCalculatorFactory.java @@ -0,0 +1,79 @@ +/* + * 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.minecraft.calculator; + +import me.lucko.luckperms.common.cacheddata.CacheMetadata; +import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorMonitored; +import me.lucko.luckperms.common.calculator.processor.DirectProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.RegexProcessor; +import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.minecraft.context.MinecraftContextManager; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import net.luckperms.api.node.Node; +import net.luckperms.api.query.QueryOptions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class MinecraftCalculatorFactory implements CalculatorFactory { + private final LuckPermsPlugin plugin; + + public MinecraftCalculatorFactory(LuckPermsPlugin plugin) { + this.plugin = plugin; + } + + @Override + public PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata) { + List processors = new ArrayList<>(5); + + processors.add(new DirectProcessor(sourceMap)); + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { + processors.add(new RegexProcessor(sourceMap)); + } + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { + processors.add(new WildcardProcessor(sourceMap)); + } + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { + processors.add(new SpongeWildcardProcessor(sourceMap)); + } + + boolean integratedOwner = queryOptions.option(MinecraftContextManager.INTEGRATED_SERVER_OWNER).orElse(false); + if (integratedOwner && this.plugin.getConfiguration().get(ConfigKeys.INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS)) { + processors.add(ServerOwnerProcessor.INSTANCE); + } + + return new PermissionCalculatorMonitored(this.plugin, metadata, processors); + } +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/calculator/ServerOwnerProcessor.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/calculator/ServerOwnerProcessor.java new file mode 100644 index 000000000..c6acd386c --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/calculator/ServerOwnerProcessor.java @@ -0,0 +1,49 @@ +/* + * 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.minecraft.calculator; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import net.luckperms.api.util.Tristate; + +/** + * Permission processor which is added to the owner of an integrated server to simply return true if no other processors match. + */ +public class ServerOwnerProcessor extends AbstractPermissionProcessor implements PermissionProcessor { + private static final TristateResult TRUE_RESULT = new TristateResult.Factory(ServerOwnerProcessor.class).result(Tristate.TRUE); + + public static final ServerOwnerProcessor INSTANCE = new ServerOwnerProcessor(); + + private ServerOwnerProcessor() { + + } + + @Override + public TristateResult hasPermission(String permission) { + return TRUE_RESULT; + } +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/command/BrigadierInjector.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/command/BrigadierInjector.java new file mode 100644 index 000000000..45fcb48cb --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/command/BrigadierInjector.java @@ -0,0 +1,191 @@ +/* + * 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.minecraft.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.luckperms.common.graph.Graph; +import me.lucko.luckperms.common.graph.TraversalAlgorithm; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsPlugin; +import me.lucko.luckperms.common.model.User; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.LevelBasedPermissionSet; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Locale; +import java.util.function.Predicate; + +/** + * Utility for injecting permission requirements into a Brigadier command tree. + */ +public final class BrigadierInjector { + private BrigadierInjector() {} + + private static final Field REQUIREMENT_FIELD; + + static { + Field requirementField; + try { + requirementField = CommandNode.class.getDeclaredField("requirement"); + requirementField.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new ExceptionInInitializerError(e); + } + REQUIREMENT_FIELD = requirementField; + } + + /** + * Inject permission requirements into the commands in the given dispatcher. + * + * @param plugin the plugin + * @param dispatcher the command dispatcher + */ + public static void inject(MinecraftLuckPermsPlugin plugin, CommandDispatcher dispatcher) { + Iterable tree = CommandNodeGraph.INSTANCE.traverse( + TraversalAlgorithm.DEPTH_FIRST_PRE_ORDER, + new CommandNodeWithParent(null, dispatcher.getRoot()) + ); + + for (CommandNodeWithParent node : tree) { + Predicate requirement = node.node.getRequirement(); + + // already injected - skip + if (requirement instanceof InjectedPermissionRequirement) { + continue; + } + + String permission = buildPermissionNode(node); + if (permission == null) { + continue; + } + + plugin.getPermissionRegistry().insert(permission); + + InjectedPermissionRequirement newRequirement = new InjectedPermissionRequirement(plugin, permission, requirement); + try { + REQUIREMENT_FIELD.set(node.node, newRequirement); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + + private static String buildPermissionNode(CommandNodeWithParent node) { + StringBuilder builder = new StringBuilder(); + + while (node != null) { + if (node.node instanceof LiteralCommandNode) { + if (!builder.isEmpty()) { + builder.insert(0, '.'); + } + + String name = node.node.getName().toLowerCase(Locale.ROOT); + builder.insert(0, name); + } + + node = node.parent; + } + + if (builder.isEmpty()) { + return null; + } + + builder.insert(0, "command."); + return builder.toString(); + } + + /** + * Injected {@link CommandNode#getRequirement() requirement} that checks for a permission, before + * delegating to the existing requirement. + */ + private static final class InjectedPermissionRequirement implements Predicate { + private final MinecraftLuckPermsPlugin plugin; + private final String permission; + private final Predicate delegate; + + private InjectedPermissionRequirement(MinecraftLuckPermsPlugin plugin, String permission, Predicate delegate) { + this.plugin = plugin; + this.permission = permission; + this.delegate = delegate; + } + + @Override + public boolean test(CommandSourceStack source) { + if (source.getEntity() instanceof ServerPlayer player) { + + User user = this.plugin.getUserManager().getIfLoaded(player.getUUID()); + if (user == null) { + return false; + } + + QueryOptions queryOptions = this.plugin.getContextManager().getQueryOptions(player); + Tristate state = user.getCachedData().getPermissionData(queryOptions).checkPermission(this.permission); + + if (state != Tristate.UNDEFINED) { + return state.asBoolean() && this.delegate.test(source.withPermission(LevelBasedPermissionSet.OWNER)); + } + } + + return this.delegate.test(source); + } + } + + /** + * A {@link Graph} to represent the brigadier command node tree. + */ + private enum CommandNodeGraph implements Graph { + INSTANCE; + + @Override + public Iterable successors(CommandNodeWithParent ctx) { + CommandNode node = ctx.node; + Collection successors = new ArrayList<>(); + + for (CommandNode child : node.getChildren()) { + successors.add(new CommandNodeWithParent(ctx, child)); + } + + return successors; + } + } + + private static final class CommandNodeWithParent { + private final CommandNodeWithParent parent; + private final CommandNode node; + + private CommandNodeWithParent(CommandNodeWithParent parent, CommandNode node) { + this.parent = parent; + this.node = node; + } + } + +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/command/MinecraftCommandExecutor.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/command/MinecraftCommandExecutor.java new file mode 100644 index 000000000..137bef8ff --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/command/MinecraftCommandExecutor.java @@ -0,0 +1,178 @@ +/* + * 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.minecraft.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.luckperms.common.command.CommandManager; +import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsPlugin; +import me.lucko.luckperms.common.sender.Sender; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.LevelBasedPermissionSet; +import net.minecraft.server.permissions.Permission; +import net.minecraft.server.permissions.PermissionLevel; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.CompletableFuture; + +public class MinecraftCommandExecutor extends CommandManager implements Command, SuggestionProvider { + private static final String[] COMMAND_ALIASES = new String[]{"luckperms", "lp", "perm", "perms", "permission", "permissions"}; + + private final MinecraftLuckPermsPlugin plugin; + + protected MinecraftCommandExecutor(MinecraftLuckPermsPlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + public void register(CommandDispatcher dispatcher) { + for (String alias : COMMAND_ALIASES) { + LiteralCommandNode command = Commands.literal(alias).executes(this).build(); + ArgumentCommandNode argument = Commands.argument("args", StringArgumentType.greedyString()) + .suggests(this) + .executes(this) + .build(); + + command.addChild(argument); + dispatcher.getRoot().addChild(command); + } + } + + @Override + public int run(CommandContext context) throws CommandSyntaxException { + CommandSourceStack source = context.getSource(); + Sender sender = this.plugin.getSenderFactory().wrap(source); + + int start = context.getRange().getStart(); + String buffer = context.getInput().substring(start); + + List arguments; + if (this.plugin.getConfiguration().get(ConfigKeys.RESOLVE_COMMAND_SELECTORS)) { + arguments = resolveSelectors(source, ArgumentTokenizer.EXECUTE.tokenizeInput(buffer)); + } else { + arguments = ArgumentTokenizer.EXECUTE.tokenizeInput(buffer); + } + + String label = arguments.remove(0); + if (label.startsWith("/")) { + label = label.substring(1); + } + + executeCommand(sender, label, arguments); + return Command.SINGLE_SUCCESS; + } + + @Override + public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) throws CommandSyntaxException { + CommandSourceStack source = context.getSource(); + Sender sender = this.plugin.getSenderFactory().wrap(source); + + int idx = builder.getStart(); + + String buffer = builder.getInput().substring(idx); + idx += buffer.length(); + + List arguments = ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(buffer); + List resolvedArguments; + if (this.plugin.getConfiguration().get(ConfigKeys.RESOLVE_COMMAND_SELECTORS)) { + resolvedArguments = resolveSelectors(source, new ArrayList<>(arguments)); + } else { + resolvedArguments = arguments; + } + + if (!arguments.isEmpty() && !resolvedArguments.isEmpty()) { + idx -= arguments.get(arguments.size() - 1).length(); + } + + List completions = tabCompleteCommand(sender, resolvedArguments); + + // Offset the builder from the current string range so suggestions are placed in the right spot + builder = builder.createOffset(idx); + for (String completion : completions) { + builder.suggest(completion); + } + return builder.buildFuture(); + } + + private List resolveSelectors(CommandSourceStack source, List args) { + // usage of @ selectors requires at least level 2 permission + + CommandSourceStack atAllowedSource = ensureSourceCanUseSelectors(source); + for (ListIterator it = args.listIterator(); it.hasNext(); ) { + String arg = it.next(); + if (arg.isEmpty() || arg.charAt(0) != '@') { + continue; + } + + List matchedPlayers; + try { + matchedPlayers = EntityArgument.entities().parse(new StringReader(arg)).findPlayers(atAllowedSource); + } catch (CommandSyntaxException e) { + this.plugin.getLogger().warn("Error parsing selector '" + arg + "' for " + source + " executing " + args, e); + continue; + } + + if (matchedPlayers.isEmpty()) { + continue; + } + + if (matchedPlayers.size() > 1) { + this.plugin.getLogger().warn("Error parsing selector '" + arg + "' for " + source + " executing " + args + + ": ambiguous result (more than one player matched) - " + matchedPlayers); + continue; + } + + ServerPlayer player = matchedPlayers.get(0); + it.set(player.getStringUUID()); + } + + return args; + } + + private static CommandSourceStack ensureSourceCanUseSelectors(CommandSourceStack source) { + if (source.permissions().hasPermission(new Permission.HasCommandLevel(PermissionLevel.GAMEMASTERS))) { + return source; + } + return source.withMaximumPermission(LevelBasedPermissionSet.GAMEMASTER); + } + +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/context/MinecraftContextManager.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/context/MinecraftContextManager.java new file mode 100644 index 000000000..28356de28 --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/context/MinecraftContextManager.java @@ -0,0 +1,36 @@ +/* + * 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.minecraft.context; + +import me.lucko.luckperms.common.context.manager.ContextManager; +import net.luckperms.api.query.OptionKey; +import net.minecraft.server.level.ServerPlayer; + +public interface MinecraftContextManager extends ContextManager { + + OptionKey INTEGRATED_SERVER_OWNER = OptionKey.of("integrated_server_owner", Boolean.class); + +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/context/MinecraftPlayerCalculator.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/context/MinecraftPlayerCalculator.java new file mode 100644 index 000000000..6a0ad1718 --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/context/MinecraftPlayerCalculator.java @@ -0,0 +1,125 @@ +/* + * 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.minecraft.context; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsPlugin; +import net.luckperms.api.context.Context; +import net.luckperms.api.context.ContextCalculator; +import net.luckperms.api.context.ContextConsumer; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.DefaultContextKeys; +import net.luckperms.api.context.ImmutableContextSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.dimension.BuiltinDimensionTypes; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Set; + +public class MinecraftPlayerCalculator implements ContextCalculator { + protected final MinecraftLuckPermsPlugin plugin; + + protected final boolean gamemode; + protected final boolean world; + protected final boolean dimensionType; + + public MinecraftPlayerCalculator(MinecraftLuckPermsPlugin plugin, Set disabled) { + this.plugin = plugin; + this.gamemode = !disabled.contains(DefaultContextKeys.GAMEMODE_KEY); + this.world = !disabled.contains(DefaultContextKeys.WORLD_KEY); + this.dimensionType = !disabled.contains(DefaultContextKeys.DIMENSION_TYPE_KEY); + } + + @Override + public void calculate(@NonNull ServerPlayer target, @NonNull ContextConsumer consumer) { + if (this.gamemode) { + GameType gameMode = target.gameMode.getGameModeForPlayer(); + consumer.accept(DefaultContextKeys.GAMEMODE_KEY, gameMode.getName()); + } + + ServerLevel level = target.level(); + if (this.dimensionType) { + consumer.accept(DefaultContextKeys.DIMENSION_TYPE_KEY, getContextKey(level.dimensionTypeRegistration().unwrapKey().orElse(BuiltinDimensionTypes.OVERWORLD).identifier())); + } + + if (this.world) { + this.plugin.getConfiguration().get(ConfigKeys.WORLD_REWRITES).rewriteAndSubmit(getContextKey(level.dimension().identifier()), consumer); + } + } + + @Override + public @NonNull ContextSet estimatePotentialContexts() { + ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); + + if (this.gamemode) { + for (GameType gameType : GameType.values()) { + builder.add(DefaultContextKeys.GAMEMODE_KEY, gameType.getName()); + } + } + + MinecraftServer server = this.plugin.getBootstrap().getServer().orElse(null); + if (this.dimensionType && server != null) { + try { + server.registryAccess().lookup(Registries.DIMENSION_TYPE).ifPresent(registry -> { + for (Identifier id : registry.keySet()) { + builder.add(DefaultContextKeys.DIMENSION_TYPE_KEY, getContextKey(id)); + } + }); + } catch (ClassCastException e) { + // Some mod loaders (observed on NeoForge 1.21.1) can return a registry + // implementation from #lookup(...) that isn't safely castable to Registry + // at this call site, e.g. a MappedRegistry$1 wrapper produced by their mixins. + // Rather than letting this crash the whole potential-context calculation + // (which is used by things like the web editor), skip the dimension-type + // contexts for this estimate. + // See: https://github.com/LuckPerms/LuckPerms/issues/4211 + } + } + + if (this.world && server != null) { + for (ServerLevel level : server.getAllLevels()) { + if (Context.isValidValue(level.dimension().identifier().toString())) { + builder.add(DefaultContextKeys.WORLD_KEY, level.dimension().identifier().toString()); + } + } + } + + return builder.build(); + } + + private static String getContextKey(Identifier key) { + if (key.getNamespace().equals("minecraft")) { + return key.getPath(); + } + return key.toString(); + } +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/listeners/MinecraftAutoOpListener.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/listeners/MinecraftAutoOpListener.java new file mode 100644 index 000000000..37041da7b --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/listeners/MinecraftAutoOpListener.java @@ -0,0 +1,68 @@ +/* + * 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.minecraft.listeners; + +import me.lucko.luckperms.common.event.listeners.AbstractAutoOpListener; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsPlugin; +import net.minecraft.server.level.ServerPlayer; + +import java.util.UUID; + +public class MinecraftAutoOpListener extends AbstractAutoOpListener, ServerPlayer> { + public MinecraftAutoOpListener(MinecraftLuckPermsPlugin plugin) { + super(plugin, plugin.getContextManager(), ServerPlayer.class); + } + + @Override + protected boolean isServerAvailable() { + return this.plugin.getBootstrap().getServer().isPresent(); + } + + @Override + protected UUID getUniqueId(ServerPlayer player) { + return player.getUUID(); + } + + @Override + protected void setOp(ServerPlayer player, boolean value, boolean callerIsSync) { + if (callerIsSync) { + setOp(player, value); + } else { + this.plugin.getBootstrap().getScheduler().executeSync(() -> setOp(player, value)); + } + } + + private void setOp(ServerPlayer player, boolean value) { + this.plugin.getBootstrap().getServer().ifPresent(server -> { + if (value) { + server.getPlayerList().op(player.nameAndId()); + } else { + server.getPlayerList().deop(player.nameAndId()); + } + }); + } + +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/listeners/MinecraftCommandListUpdater.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/listeners/MinecraftCommandListUpdater.java new file mode 100644 index 000000000..b1f7c2dfd --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/listeners/MinecraftCommandListUpdater.java @@ -0,0 +1,65 @@ +/* + * 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.minecraft.listeners; + +import me.lucko.luckperms.common.event.listeners.AbstractCommandListUpdater; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsPlugin; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; + +import java.util.UUID; + +/** + * Calls {@link PlayerList#sendPlayerPermissionLevel(ServerPlayer)} when a players permissions change. + */ +public class MinecraftCommandListUpdater extends AbstractCommandListUpdater, ServerPlayer> { + public MinecraftCommandListUpdater(MinecraftLuckPermsPlugin plugin) { + super(plugin, ServerPlayer.class); + } + + @Override + protected boolean isServerAvailable() { + return this.plugin.getBootstrap().getServer().isPresent(); + } + + @Override + protected UUID getUniqueId(ServerPlayer player) { + return player.getUUID(); + } + + @Override + protected void sendCommandListUpdate(UUID uniqueId) { + this.plugin.getBootstrap().getScheduler().executeSync(() -> { + ServerPlayer player = this.plugin.getBootstrap().getPlayer(uniqueId).orElse(null); + if (player != null) { + MinecraftServer server = player.level().getServer(); + server.getPlayerList().sendPlayerPermissionLevel(player); + } + }); + } + +} diff --git a/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/util/AbstractAsyncConfigurationTask.java b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/util/AbstractAsyncConfigurationTask.java new file mode 100644 index 000000000..94d77162a --- /dev/null +++ b/common/minecraft/src/main/java/me/lucko/luckperms/common/minecraft/util/AbstractAsyncConfigurationTask.java @@ -0,0 +1,59 @@ +/* + * 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.minecraft.util; + +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsPlugin; +import net.minecraft.server.network.ConfigurationTask; + +import java.util.concurrent.CompletableFuture; + +public abstract class AbstractAsyncConfigurationTask implements ConfigurationTask { + private final MinecraftLuckPermsPlugin plugin; + private final Type type; + private final Runnable task; + + public AbstractAsyncConfigurationTask(MinecraftLuckPermsPlugin plugin, Type type, Runnable task) { + this.plugin = plugin; + this.type = type; + this.task = task; + } + + protected CompletableFuture start(Runnable completeCallback) { + CompletableFuture future = CompletableFuture.runAsync(this.task, this.plugin.getBootstrap().getScheduler().async()); + future.whenCompleteAsync((o, e) -> { + if (e != null) { + this.plugin.getLogger().warn("Configuration task threw an exception", e); + } + completeCallback.run(); + }, this.plugin.getBootstrap().getScheduler().sync()); + return future; + } + + @Override + public Type type() { + return this.type; + } +} \ No newline at end of file diff --git a/common/placeholders/build.gradle b/common/placeholders/build.gradle new file mode 100644 index 000000000..155198238 --- /dev/null +++ b/common/placeholders/build.gradle @@ -0,0 +1,45 @@ +plugins { + id("java-library") + id("jacoco") +} + +test { + useJUnitPlatform() +} + +jacocoTestReport { + dependsOn test +} + +dependencies { + api project(':api') + + compileOnly 'org.checkerframework:checker-qual:3.49.3' + compileOnly 'org.jetbrains:annotations:26.0.2' + + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation 'org.testcontainers:testcontainers-junit-jupiter:2.0.2' + testImplementation 'org.mockito:mockito-core:5.18.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0' +} + +publishing { + //repositories { + // maven { + // url = 'https://nexus.lucko.me/repository/maven-snapshots/' + // credentials { + // username = luckoNexusUsername + // password = luckoNexusPassword + // } + // } + //} + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = 'common-placeholders' + version = "${project.ext.fullVersion}-SNAPSHOT" + } + } +} \ No newline at end of file diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholder.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholder.java new file mode 100644 index 000000000..dbb7e6faf --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholder.java @@ -0,0 +1,117 @@ +/* + * 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.placeholders; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.jetbrains.annotations.NotNull; + +/** + * A placeholder definition. + */ +public interface Placeholder { + + /** + * Get the id of the placeholder. + * + * @return the id + */ + @NonNull String id(); + + /** + * A placeholder function that uses the standard {@link PlaceholderContext} + */ + interface BasicPlaceholderFunction { + /** + * Resolve the value of this placeholder with some given context. + * + * @param ctx the context + * @return the resolved value + */ + @NonNull String resolve(@NonNull PlaceholderContext ctx); + } + + /** + * A placeholder function that uses the extended {@link PlaceholderContext.WithArgument} + */ + interface UsingArgumentPlaceholderFunction { + /** + * Resolve the value of this placeholder with some given context. + * + * @param ctx the context + * @return the resolved value + */ + @NonNull String resolve(PlaceholderContext.@NonNull WithArgument ctx); + } + + /** A basic placeholder */ + interface Basic extends Placeholder, BasicPlaceholderFunction {} + + /** A placeholder that uses an argument */ + interface UsingArgument extends Placeholder, UsingArgumentPlaceholderFunction {} + + /** + * Create a standard placeholder using the given resolver function. + * + * @param id the id + * @param resolver the resolver function + * @return the placeholder + */ + static Basic basic(@NonNull String id, Placeholder.@NonNull BasicPlaceholderFunction resolver) { + return new Basic() { + @Override + public @NonNull String id() { + return id; + } + + @Override + public @NotNull String resolve(@NotNull PlaceholderContext ctx) { + return resolver.resolve(ctx); + } + }; + } + + /** + * Create a dynamic placeholder using the given resolver function. + * + * @param id the id + * @param resolver the resolver function + * @return the placeholder + */ + static UsingArgument usingArgument(@NonNull String id, Placeholder.@NonNull UsingArgumentPlaceholderFunction resolver) { + return new UsingArgument() { + @Override + public @NonNull String id() { + return id; + } + + @Override + public @NotNull String resolve(PlaceholderContext.@NotNull WithArgument ctx) { + return resolver.resolve(ctx); + } + }; + } + +} diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderContext.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderContext.java new file mode 100644 index 000000000..d1b2bd75d --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderContext.java @@ -0,0 +1,110 @@ +/* + * 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.placeholders; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.cacheddata.CachedDataManager; +import net.luckperms.api.cacheddata.CachedMetaData; +import net.luckperms.api.cacheddata.CachedPermissionData; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * The context passed to a {@link Placeholder} resolve request. + */ +public class PlaceholderContext { + + /** The LuckPerms API instance */ + private final @NonNull LuckPerms api; + /** The user for the player the placeholder is being resolved for */ + private final @NonNull User user; + /** The query options for the player the placeholder is being resolved for */ + private final @NonNull QueryOptions queryOptions; + + public PlaceholderContext(@NonNull LuckPerms api, @NonNull User user, @NonNull QueryOptions queryOptions) { + this.api = api; + this.user = user; + this.queryOptions = queryOptions; + } + + public @NonNull LuckPerms api() { + return this.api; + } + + public @NonNull User user() { + return this.user; + } + + public @NonNull QueryOptions queryOptions() { + return this.queryOptions; + } + + public @NonNull CachedDataManager userData() { + return this.user.getCachedData(); + } + + public @NonNull CachedPermissionData permissionData() { + return this.user.getCachedData().getPermissionData(this.queryOptions); + } + + public @NonNull CachedMetaData metaData() { + return this.user.getCachedData().getMetaData(this.queryOptions); + } + + /** + * Create a copy of this placeholder context, additionally including an argument. + * + * @param argument the argument + * @return the new context + */ + public WithArgument withArgument(@NonNull String argument) { + return new WithArgument(this.api, this.user, this.queryOptions, argument); + } + + /** + * Extension of {@link PlaceholderContext} with an extra dynamic argument provided by the requester. + */ + public static class WithArgument extends PlaceholderContext { + + /** An additional argument passed to the placeholder resolve request */ + private final @NonNull String argument; + + public WithArgument(@NonNull LuckPerms api, @NonNull User user, @NonNull QueryOptions queryOptions, @NonNull String argument) { + super(api, user, queryOptions); + this.argument = argument; + } + + /** + * Gets the argument. + * + * @return the argument + */ + public @NonNull String argument() { + return this.argument; + } + } +} diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistry.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistry.java new file mode 100644 index 000000000..bde519859 --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistry.java @@ -0,0 +1,100 @@ +/* + * 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.placeholders; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * A registry of standard/built-in {@link Placeholder}s. + */ +public class PlaceholderRegistry { + + private static final List PLACEHOLDERS = Collections.unmodifiableList(Arrays.asList( + Placeholders.PREFIX, + Placeholders.SUFFIX, + Placeholders.ALL_META, + Placeholders.META, + Placeholders.PREFIX_ELEMENT, + Placeholders.SUFFIX_ELEMENT, + Placeholders.ALL_CONTEXT, + Placeholders.CONTEXT, + Placeholders.GROUPS, + Placeholders.INHERITED_GROUPS, + Placeholders.PRIMARY_GROUP_NAME, + Placeholders.HAS_PERMISSION, + Placeholders.INHERITS_PERMISSION, + Placeholders.CHECK_PERMISSION, + Placeholders.IN_GROUP, + Placeholders.INHERITS_GROUP, + Placeholders.ON_TRACK, + Placeholders.HAS_GROUPS_ON_TRACK, + Placeholders.HIGHEST_GROUP_BY_WEIGHT, + Placeholders.LOWEST_GROUP_BY_WEIGHT, + Placeholders.HIGHEST_INHERITED_GROUP_BY_WEIGHT, + Placeholders.LOWEST_INHERITED_GROUP_BY_WEIGHT, + Placeholders.HIGHEST_GROUP_WEIGHT, + Placeholders.CURRENT_GROUP_ON_TRACK, + Placeholders.NEXT_GROUP_ON_TRACK, + Placeholders.PREVIOUS_GROUP_ON_TRACK, + Placeholders.FIRST_GROUP_ON_TRACKS, + Placeholders.LAST_GROUP_ON_TRACKS, + Placeholders.EXPIRY_TIME, + Placeholders.INHERITED_EXPIRY_TIME, + Placeholders.GROUP_EXPIRY_TIME, + Placeholders.INHERITED_GROUP_EXPIRY_TIME + )); + + private static final Map PLACEHOLDER_MAP = PLACEHOLDERS.stream() + .collect(Collectors.toMap(Placeholder::id, Function.identity())); + + /** + * Get a list of all placeholders. + * + * @return a list of placeholders + */ + public static @NonNull List getAll() { + return PLACEHOLDERS; + } + + /** + * Lookup a placeholder by id. + * + * @param id the id to lookup + * @return the placeholder, if found + */ + public static @Nullable Placeholder lookup(@NonNull String id) { + return PLACEHOLDER_MAP.get(id); + } + +} diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderResolver.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderResolver.java new file mode 100644 index 000000000..5eed8ad0e --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/PlaceholderResolver.java @@ -0,0 +1,106 @@ +/* + * 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.placeholders; + +import java.util.Collection; +import java.util.Locale; + +/** + * Resolves placeholders using a simple string format: + * + *

    + *

      + *
    • placeholder
    • + *
    • placeholder_argument
    • + *
    + *

    + * + *

    Note: this resolver does not parse placeholders mid-string, it expects to receive the parsed + * placeholder string as input.

    + */ +public class PlaceholderResolver { + + /** The placeholders used by this resolver */ + private final Collection placeholders; + + /** + * Create a resolver using the built-in placeholders registered in {@link PlaceholderRegistry}. + */ + public PlaceholderResolver() { + this(PlaceholderRegistry.getAll()); + } + + /** + * Create a resolver using a custom list of placeholders. + * + * @param placeholders the placeholders + */ + public PlaceholderResolver(Collection placeholders) { + this.placeholders = placeholders; + } + + /** + * Resolve the placeholder value of a given input string + * + * @param input the input string + * @return the resolved value, or null if no placeholder matched + */ + public String resolve(PlaceholderContext ctx, String input) { + input = input.toLowerCase(Locale.ROOT); + for (Placeholder placeholder : this.placeholders) { + String result = attemptResolve(ctx, input, placeholder); + if (result != null) { + return result; + } + } + return null; + } + + /** + * Attempt to resolve the placeholder value of a given input string for a specific placeholder. + * + * @param ctx the placeholder context + * @param input the input string + * @param placeholder the placeholder to attempt to resolve with + * @return the resolved value if the placeholder matches the input, or null if it does not match + */ + protected String attemptResolve(PlaceholderContext ctx, String input, Placeholder placeholder) { + String id = placeholder.id(); + if (placeholder instanceof Placeholder.Basic) { + if (input.equals(id)) { + return ((Placeholder.Basic) placeholder).resolve(ctx); + } + } else if (placeholder instanceof Placeholder.UsingArgument) { + if (input.startsWith(id + "_") && input.length() > (id.length() + 1)) { + String argument = input.substring(id.length() + 1); + return ((Placeholder.UsingArgument) placeholder).resolve(ctx.withArgument(argument)); + } + } else { + throw new IllegalArgumentException("Unknown placeholder type: " + placeholder.getClass()); + } + return null; + } +} diff --git a/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholders.java b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholders.java new file mode 100644 index 000000000..0979aed19 --- /dev/null +++ b/common/placeholders/src/main/java/me/lucko/luckperms/common/placeholders/Placeholders.java @@ -0,0 +1,461 @@ +/* + * 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.placeholders; + +import net.luckperms.api.metastacking.DuplicateRemovalFunction; +import net.luckperms.api.metastacking.MetaStackDefinition; +import net.luckperms.api.metastacking.MetaStackElement; +import net.luckperms.api.model.group.Group; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.track.Track; +import org.jetbrains.annotations.VisibleForTesting; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A class containing the standard, built-in placeholders. + */ +public final class Placeholders { + private Placeholders() { + throw new AssertionError(); + } + + // Basic meta placeholders + + /** Outputs the user's prefix. */ + public static final Placeholder.Basic PREFIX = Placeholder.basic("prefix", (ctx) -> stringNullToEmpty(ctx.metaData().getPrefix())); + + /** Outputs the user's suffix. */ + public static final Placeholder.Basic SUFFIX = Placeholder.basic("suffix", (ctx) -> stringNullToEmpty(ctx.metaData().getSuffix())); + + /** Outputs all meta values for a given key, separated by commas. */ + public static final Placeholder.UsingArgument ALL_META = Placeholder.usingArgument("all_meta", (ctx) -> { + List values = ctx.metaData().getMeta().getOrDefault(ctx.argument(), Collections.emptyList()); + return values.isEmpty() ? "" : String.join(", ", values); + }); + + /** Outputs a specific meta value for a given key. */ + public static final Placeholder.UsingArgument META = Placeholder.usingArgument("meta", (ctx) -> stringNullToEmpty(ctx.metaData().getMetaValue(ctx.argument()))); + + // Meta stack element placeholders + /** Outputs the user's prefix from a specific meta stack element. */ + public static final Placeholder.UsingArgument PREFIX_ELEMENT = Placeholder.usingArgument("prefix_element", (ctx) -> { + MetaStackElement stackElement = ctx.api().getMetaStackFactory().fromString(ctx.argument()).orElse(null); + if (stackElement == null) { + throw new IllegalArgumentException("Invalid meta stack element: " + ctx.argument()); + } + + MetaStackDefinition stackDefinition = ctx.api().getMetaStackFactory().createDefinition( + Collections.singletonList(stackElement), DuplicateRemovalFunction.RETAIN_ALL, "", "", ""); + QueryOptions newOptions = ctx.queryOptions().toBuilder() + .option(MetaStackDefinition.PREFIX_STACK_KEY, stackDefinition) + .option(MetaStackDefinition.SUFFIX_STACK_KEY, stackDefinition) + .build(); + + return stringNullToEmpty(ctx.userData().getMetaData(newOptions).getPrefix()); + }); + + /** Outputs the user's suffix from a specific meta stack element. */ + public static final Placeholder.UsingArgument SUFFIX_ELEMENT = Placeholder.usingArgument("suffix_element", (ctx) -> { + MetaStackElement stackElement = ctx.api().getMetaStackFactory().fromString(ctx.argument()).orElse(null); + if (stackElement == null) { + throw new IllegalArgumentException("Invalid meta stack element: " + ctx.argument()); + } + + MetaStackDefinition stackDefinition = ctx.api().getMetaStackFactory().createDefinition( + Collections.singletonList(stackElement), DuplicateRemovalFunction.RETAIN_ALL, "", "", ""); + QueryOptions newOptions = ctx.queryOptions().toBuilder() + .option(MetaStackDefinition.PREFIX_STACK_KEY, stackDefinition) + .option(MetaStackDefinition.SUFFIX_STACK_KEY, stackDefinition) + .build(); + + return Objects.toString(ctx.userData().getMetaData(newOptions).getSuffix(), ""); + }); + + // Context placeholders + + /** Outputs all context key-value pairs, separated by commas. */ + public static final Placeholder.Basic ALL_CONTEXT = Placeholder.basic("all_context", (ctx) -> + ctx.queryOptions().context().toSet().stream() + .map(c -> c.getKey() + "=" + c.getValue()) + .collect(Collectors.joining(", ")) + ); + + /** Outputs all values for a specific context key, separated by commas. */ + public static final Placeholder.UsingArgument CONTEXT = Placeholder.usingArgument("context", (ctx) -> String.join(", ", ctx.queryOptions().context().getValues(ctx.argument()))); + + // Group placeholders + + /** Outputs the user's directly assigned groups, separated by commas. */ + public static final Placeholder.Basic GROUPS = Placeholder.basic("groups", (ctx) -> + ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .map(name -> convertGroupDisplayName(ctx, name)) + .collect(Collectors.joining(", ")) + ); + + /** Outputs all groups the user inherits from, separated by commas. */ + public static final Placeholder.Basic INHERITED_GROUPS = Placeholder.basic("inherited_groups", (ctx) -> + ctx.user().getInheritedGroups(ctx.queryOptions()).stream() + .map(Group::getFriendlyName) + .collect(Collectors.joining(", ")) + ); + + /** Outputs the user's primary group name. */ + public static final Placeholder.Basic PRIMARY_GROUP_NAME = Placeholder.basic("primary_group_name", (ctx) -> + convertGroupDisplayName(ctx, ctx.user().getPrimaryGroup()) + ); + + // Permission check placeholders + /** Checks if the user has a specific permission node directly assigned (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument HAS_PERMISSION = Placeholder.usingArgument("has_permission", (ctx) -> + String.valueOf(ctx.user().getNodes().stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .anyMatch(n -> n.getKey().equals(ctx.argument()))) + ); + + /** Checks if the user inherits a specific permission node (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument INHERITS_PERMISSION = Placeholder.usingArgument("inherits_permission", (ctx) -> + String.valueOf(ctx.user().resolveInheritedNodes(ctx.queryOptions()).stream() + .filter(n -> n.getContexts().isSatisfiedBy(ctx.queryOptions().context())) + .anyMatch(n -> n.getKey().equals(ctx.argument()))) + ); + + /** Checks the result of a permission check (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument CHECK_PERMISSION = Placeholder.usingArgument("check_permission", (ctx) -> + String.valueOf(ctx.permissionData().checkPermission(ctx.argument()).asBoolean()) + ); + + /** Checks if the user is directly in a specific group (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument IN_GROUP = Placeholder.usingArgument("in_group", (ctx) -> + String.valueOf(ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .anyMatch(s -> s.equalsIgnoreCase(ctx.argument()))) + ); + + /** Checks if the user inherits from a specific group (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument INHERITS_GROUP = Placeholder.usingArgument("inherits_group", (ctx) -> + String.valueOf(ctx.user().getInheritedGroups(ctx.queryOptions()).stream() + .anyMatch(g -> g.getName().equalsIgnoreCase(ctx.argument()))) + ); + + // Track placeholders + /** Checks if the user's primary group is on a specific track (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument ON_TRACK = Placeholder.usingArgument("on_track", (ctx) -> + String.valueOf(Optional.ofNullable(ctx.api().getTrackManager().getTrack(ctx.argument())) + .map(t -> t.containsGroup(ctx.user().getPrimaryGroup())) + .orElse(false)) + ); + + /** Checks if the user has any groups on a specific track (outputs "true" or "false"). */ + public static final Placeholder.UsingArgument HAS_GROUPS_ON_TRACK = Placeholder.usingArgument("has_groups_on_track", (ctx) -> + String.valueOf(Optional.ofNullable(ctx.api().getTrackManager().getTrack(ctx.argument())) + .map(t -> ctx.user().getNodes(NodeType.INHERITANCE).stream() + .map(InheritanceNode::getGroupName) + .anyMatch(t::containsGroup) + ) + .orElse(false)) + ); + + // Group weight placeholders + + /** Outputs the name of the user's highest weighted directly assigned group. */ + public static final Placeholder.Basic HIGHEST_GROUP_BY_WEIGHT = Placeholder.basic("highest_group_by_weight", (ctx) -> + ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .map(n -> ctx.api().getGroupManager().getGroup(n)) + .filter(Objects::nonNull) + .max(Comparator.comparingInt(g -> g.getWeight().orElse(0))) + .map(Group::getName) + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse("") + ); + + /** Outputs the name of the user's lowest weighted directly assigned group. */ + public static final Placeholder.Basic LOWEST_GROUP_BY_WEIGHT = Placeholder.basic("lowest_group_by_weight", (ctx) -> + ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .map(n -> ctx.api().getGroupManager().getGroup(n)) + .filter(Objects::nonNull) + .min(Comparator.comparingInt(g -> g.getWeight().orElse(0))) + .map(Group::getName) + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse("") + ); + + /** Outputs the name of the user's highest weighted inherited group. */ + public static final Placeholder.Basic HIGHEST_INHERITED_GROUP_BY_WEIGHT = Placeholder.basic("highest_inherited_group_by_weight", (ctx) -> + ctx.user().getInheritedGroups(ctx.queryOptions()).stream() + .max(Comparator.comparingInt(g -> g.getWeight().orElse(0))) + .map(Group::getName) + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse("") + ); + + /** Outputs the name of the user's lowest weighted inherited group. */ + public static final Placeholder.Basic LOWEST_INHERITED_GROUP_BY_WEIGHT = Placeholder.basic("lowest_inherited_group_by_weight", (ctx) -> + ctx.user().getInheritedGroups(ctx.queryOptions()).stream() + .min(Comparator.comparingInt(g -> g.getWeight().orElse(0))) + .map(Group::getName) + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse("") + ); + + /** Outputs the weight value of the user's highest weighted directly assigned group. */ + public static final Placeholder.Basic HIGHEST_GROUP_WEIGHT = Placeholder.basic("highest_group_weight", (ctx) -> + String.valueOf(ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(InheritanceNode::getGroupName) + .map(n -> ctx.api().getGroupManager().getGroup(n)) + .filter(Objects::nonNull) + .map(Group::getWeight) + .filter(OptionalInt::isPresent) + .mapToInt(OptionalInt::getAsInt) + .max() + .orElse(0)) + ); + + // Track position placeholders + + /** Outputs the user's current group on a specific track. */ + public static final Placeholder.UsingArgument CURRENT_GROUP_ON_TRACK = Placeholder.usingArgument("current_group_on_track", (ctx) -> { + Track track = ctx.api().getTrackManager().getTrack(ctx.argument()); + if (track == null) { + return ""; + } + + List groups = ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> track.containsGroup(n.getGroupName())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .distinct() + .map(n -> ctx.api().getGroupManager().getGroup(n.getGroupName())) + .collect(Collectors.toList()); + + if (groups.size() != 1) { + return ""; + } + + return groups.get(0).getFriendlyName(); + }); + + /** Outputs the next group on a specific track. */ + public static final Placeholder.UsingArgument NEXT_GROUP_ON_TRACK = Placeholder.usingArgument("next_group_on_track", (ctx) -> { + Track track = ctx.api().getTrackManager().getTrack(ctx.argument()); + if (track == null || track.getGroups().size() <= 1) { + return ""; + } + + List groups = ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> track.containsGroup(n.getGroupName())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .distinct() + .map(n -> ctx.api().getGroupManager().getGroup(n.getGroupName())) + .collect(Collectors.toList()); + + if (groups.size() != 1) { + return ""; + } + + return Objects.toString(convertGroupDisplayName(ctx, track.getNext(groups.get(0))), ""); + }); + + /** Outputs the previous group on a specific track. */ + public static final Placeholder.UsingArgument PREVIOUS_GROUP_ON_TRACK = Placeholder.usingArgument("previous_group_on_track", (ctx) -> { + Track track = ctx.api().getTrackManager().getTrack(ctx.argument()); + if (track == null || track.getGroups().size() <= 1) { + return ""; + } + + List groups = ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(n -> track.containsGroup(n.getGroupName())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .distinct() + .map(n -> ctx.api().getGroupManager().getGroup(n.getGroupName())) + .collect(Collectors.toList()); + + if (groups.size() != 1) { + return ""; + } + + return Objects.toString(convertGroupDisplayName(ctx, track.getPrevious(groups.get(0))), ""); + }); + + /** Outputs the first group the user has on a comma-separated list of tracks. */ + public static final Placeholder.UsingArgument FIRST_GROUP_ON_TRACKS = Placeholder.usingArgument("first_group_on_tracks", (ctx) -> { + List tracks = Arrays.stream(ctx.argument().split(",")).map(String::trim).collect(Collectors.toList()); + Set groups = ctx.user().getInheritedGroups(ctx.queryOptions()).stream().map(Group::getName).collect(Collectors.toSet()); + + return tracks.stream() + .map(n -> ctx.api().getTrackManager().getTrack(n)) + .filter(Objects::nonNull) + .map(Track::getGroups) + .map(trackGroups -> trackGroups.stream().filter(groups::contains).findFirst()) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse(""); + }); + + /** Outputs the last group the user has on a comma-separated list of tracks. */ + public static final Placeholder.UsingArgument LAST_GROUP_ON_TRACKS = Placeholder.usingArgument("last_group_on_tracks", (ctx) -> { + List tracks = Arrays.stream(ctx.argument().split(",")).map(String::trim).collect(Collectors.toList()); + Set groups = ctx.user().getInheritedGroups(ctx.queryOptions()).stream().map(Group::getName).collect(Collectors.toSet()); + + return tracks.stream() + .map(n -> ctx.api().getTrackManager().getTrack(n)) + .filter(Objects::nonNull) + .map(Track::getGroups) + .map(list -> { + List copy = new ArrayList<>(list); + Collections.reverse(copy); + return copy; + }) + .map(trackGroups -> trackGroups.stream().filter(groups::contains).findFirst()) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst() + .map(name -> convertGroupDisplayName(ctx, name)) + .orElse(""); + }); + + // Expiry time placeholders + + /** Outputs the expiry time remaining for a specific permission node. */ + public static final Placeholder.UsingArgument EXPIRY_TIME = Placeholder.usingArgument("expiry_time", (ctx) -> + ctx.user().getNodes().stream() + .filter(Node::hasExpiry) + .filter(n -> n.getKey().equals(ctx.argument())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(Node::getExpiryDuration) + .filter(Objects::nonNull) + .filter(d -> !d.isNegative()) + .findFirst() + .map(Placeholders::formatDuration) + .orElse("") + ); + + /** Outputs the expiry time remaining for a specific inherited permission node. */ + public static final Placeholder.UsingArgument INHERITED_EXPIRY_TIME = Placeholder.usingArgument("inherited_expiry_time", (ctx) -> + ctx.user().resolveInheritedNodes(ctx.queryOptions()).stream() + .filter(Node::hasExpiry) + .filter(n -> n.getKey().equals(ctx.argument())) + .map(Node::getExpiryDuration) + .filter(Objects::nonNull) + .filter(d -> !d.isNegative()) + .findFirst() + .map(Placeholders::formatDuration) + .orElse("") + ); + + /** Outputs the expiry time remaining for a specific group membership. */ + public static final Placeholder.UsingArgument GROUP_EXPIRY_TIME = Placeholder.usingArgument("group_expiry_time", (ctx) -> + ctx.user().getNodes(NodeType.INHERITANCE).stream() + .filter(Node::hasExpiry) + .filter(n -> n.getGroupName().equals(ctx.argument())) + .filter(n -> ctx.queryOptions().satisfies(n.getContexts())) + .map(Node::getExpiryDuration) + .filter(Objects::nonNull) + .filter(d -> !d.isNegative()) + .findFirst() + .map(Placeholders::formatDuration) + .orElse("") + ); + + /** Outputs the expiry time remaining for a specific inherited group membership. */ + public static final Placeholder.UsingArgument INHERITED_GROUP_EXPIRY_TIME = Placeholder.usingArgument("inherited_group_expiry_time", (ctx) -> + ctx.user().resolveInheritedNodes(ctx.queryOptions()).stream() + .filter(Node::hasExpiry) + .filter(NodeType.INHERITANCE::matches) + .map(NodeType.INHERITANCE::cast) + .filter(n -> n.getGroupName().equals(ctx.argument())) + .map(Node::getExpiryDuration) + .filter(Objects::nonNull) + .filter(d -> !d.isNegative()) + .findFirst() + .map(Placeholders::formatDuration) + .orElse("") + ); + + private static String stringNullToEmpty(String string) { + return string == null ? "" : string; + } + + private static String convertGroupDisplayName(PlaceholderContext ctx, String groupName) { + Group group = ctx.api().getGroupManager().getGroup(groupName); + return group != null ? group.getFriendlyName() : groupName; + } + + // simple version of me.lucko.luckperms.common.util.DurationFormatter + @VisibleForTesting + static String formatDuration(Duration duration) { + if (duration == null || duration.isNegative()) { + return ""; + } + + long seconds = duration.getSeconds(); + StringBuilder builder = new StringBuilder(); + + ChronoUnit[] units = {ChronoUnit.YEARS, ChronoUnit.MONTHS, ChronoUnit.WEEKS, + ChronoUnit.DAYS, ChronoUnit.HOURS, ChronoUnit.MINUTES, ChronoUnit.SECONDS}; + String[] labels = {"y", "mo", "w", "d", "h", "m", "s"}; + + for (int i = 0; i < units.length; i++) { + long unitSeconds = units[i].getDuration().getSeconds(); + long n = seconds / unitSeconds; + if (n > 0) { + seconds -= unitSeconds * n; + if (builder.length() > 0) { + builder.append(" "); + } + builder.append(n).append(labels[i]); + } + if (seconds <= 0) { + break; + } + } + + return builder.length() == 0 ? "0s" : builder.toString(); + } +} diff --git a/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistryTest.java b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistryTest.java new file mode 100644 index 000000000..acce7a732 --- /dev/null +++ b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderRegistryTest.java @@ -0,0 +1,88 @@ +/* + * 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.placeholders; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class PlaceholderRegistryTest { + + @Test + public void testAllPlaceholdersAreRegistered() { + List returnedByAllMethod = PlaceholderRegistry.getAll(); + Map inClass = Arrays.stream(Placeholders.class.getDeclaredFields()) + .filter(f -> Placeholder.class.isAssignableFrom(f.getType())) + .collect(Collectors.toMap(Field::getName, f -> { + try { + return (Placeholder) f.get(null); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }, (a, b) -> { throw new UnsupportedOperationException(); }, LinkedHashMap::new)); + + assertEquals(new ArrayList<>(inClass.values()), returnedByAllMethod); + inClass.forEach((fieldName, placeholder) -> + assertEquals(fieldName.toLowerCase(Locale.ROOT), placeholder.id(), "Placeholder " + fieldName + " has an id that doesn't match its field name") + ); + } + + @Test + public void testPlaceholdersDontOverlap() { + for (Placeholder placeholder : PlaceholderRegistry.getAll()) { + for (Placeholder other : PlaceholderRegistry.getAll()) { + if (placeholder == other) { + continue; + } + + assertNotEquals(placeholder.id(), other.id(), "Placeholder " + placeholder + " has the same id as " + other); + assertFalse(other instanceof Placeholder.UsingArgument && placeholder.id().startsWith(other.id()), "Placeholder " + placeholder.id() + " has an id that overlaps with " + other.id()); + } + } + } + + @Test + public void testRegistryLookup() { + Placeholder value = PlaceholderRegistry.lookup("prefix_element"); + assertSame(Placeholders.PREFIX_ELEMENT, value); + + assertNull(PlaceholderRegistry.lookup("non_existent")); + } + +} diff --git a/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderResolverTest.java b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderResolverTest.java new file mode 100644 index 000000000..cdfe998a4 --- /dev/null +++ b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderResolverTest.java @@ -0,0 +1,69 @@ +/* + * 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.placeholders; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; + +public class PlaceholderResolverTest { + + private final List placeholders = Arrays.asList( + Placeholder.basic("test_simple", ctx -> "hello world"), + Placeholder.usingArgument("test_arg", ctx -> "hello " + ctx.argument()) + ); + private final PlaceholderContext ctx = new PlaceholderContext(mock(LuckPerms.class), mock(User.class), mock(QueryOptions.class)); + private final PlaceholderResolver resolver = new PlaceholderResolver(this.placeholders); + + @Test + public void testNullResolve() { + assertNull(this.resolver.resolve(this.ctx, "non_existent")); + assertNull(this.resolver.resolve(this.ctx, "")); + + assertNull(this.resolver.resolve(this.ctx, "test_simple_hello")); // reject basic with arg provided + assertNull(this.resolver.resolve(this.ctx, "test_arg")); // reject usingArgument without arg provided + assertNull(this.resolver.resolve(this.ctx, "test_arg_")); // reject usingArgument without arg provided + } + + @Test + public void testBasicResolve() { + assertEquals("hello world", this.resolver.resolve(this.ctx, "test_simple")); + } + + @Test + public void testUsingArgumentResolve() { + assertEquals("hello there", this.resolver.resolve(this.ctx, "test_arg_there")); + } + +} diff --git a/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderTest.java b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderTest.java new file mode 100644 index 000000000..362e492f8 --- /dev/null +++ b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholderTest.java @@ -0,0 +1,56 @@ +/* + * 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.placeholders; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class PlaceholderTest { + + private final PlaceholderContext ctx = new PlaceholderContext(mock(LuckPerms.class), mock(User.class), mock(QueryOptions.class)); + + @Test + public void testBasic() { + Placeholder.Basic placeholder = Placeholder.basic("test", ctx -> "hello"); + + assertEquals("test", placeholder.id()); + assertEquals("hello", placeholder.resolve(this.ctx)); + } + + @Test + public void testUsingArgument() { + Placeholder.UsingArgument placeholder = Placeholder.usingArgument("test", ctx -> "hello " + ctx.argument()); + + assertEquals("test", placeholder.id()); + assertEquals("hello world", placeholder.resolve(this.ctx.withArgument("world"))); + } + +} diff --git a/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholdersTest.java b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholdersTest.java new file mode 100644 index 000000000..d1ca22a1b --- /dev/null +++ b/common/placeholders/src/test/java/me/lucko/luckperms/common/placeholders/PlaceholdersTest.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.placeholders; + +import net.luckperms.api.LuckPerms; +import net.luckperms.api.cacheddata.CachedDataManager; +import net.luckperms.api.cacheddata.CachedMetaData; +import net.luckperms.api.cacheddata.CachedPermissionData; +import net.luckperms.api.model.user.User; +import net.luckperms.api.query.QueryOptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class PlaceholdersTest { + + @Mock private LuckPerms api; + @Mock private User user; + @Mock private QueryOptions queryOptions; + + @Mock private CachedDataManager cachedDataManager; + @Mock private CachedPermissionData cachedPermissionData; + @Mock private CachedMetaData cachedMetaData; + + private PlaceholderContext ctx; + + @BeforeEach + public void setupMocks() { + lenient().when(this.user.getCachedData()).thenReturn(this.cachedDataManager); + lenient().when(this.cachedDataManager.getPermissionData(this.queryOptions)).thenReturn(this.cachedPermissionData); + lenient().when(this.cachedDataManager.getMetaData(this.queryOptions)).thenReturn(this.cachedMetaData); + + this.ctx = new PlaceholderContext(this.api, this.user, this.queryOptions); + } + + // test some of the basic / simple / most used placeholders - the others are too difficult to test + // using mocks only. + + @Test + public void testPrefix() { + when(this.cachedMetaData.getPrefix()).thenReturn("test prefix"); + assertEquals("test prefix", Placeholders.PREFIX.resolve(this.ctx)); + } + + @Test + public void testSuffix() { + when(this.cachedMetaData.getSuffix()).thenReturn("test suffix"); + assertEquals("test suffix", Placeholders.SUFFIX.resolve(this.ctx)); + } + + @Test + public void testMeta() { + when(this.cachedMetaData.getMetaValue("test_key")).thenReturn("hello"); + assertEquals("hello", Placeholders.META.resolve(this.ctx.withArgument("test_key"))); + } + + @Test + public void testFormatDuration() { + Duration duration = ChronoUnit.YEARS.getDuration().multipliedBy(5) + .plus(ChronoUnit.MONTHS.getDuration().multipliedBy(4)) + .plus(ChronoUnit.WEEKS.getDuration().multipliedBy(3)) + .plusDays(2) + .plusHours(1) + .plusMinutes(6) + .plusSeconds(7); + + assertEquals("5y 4mo 3w 2d 1h 6m 7s", Placeholders.formatDuration(duration)); + assertEquals("1m 10s", Placeholders.formatDuration(Duration.ofMinutes(1).plusSeconds(10))); + assertEquals("0s", Placeholders.formatDuration(Duration.ZERO)); + } + +} diff --git a/common/placeholders/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/common/placeholders/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/common/placeholders/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/common/src/main/java-templates/me/lucko/luckperms/common/util/BuildInfo.java.peb b/common/src/main/java-templates/me/lucko/luckperms/common/util/BuildInfo.java.peb new file mode 100644 index 000000000..1d9b0c9cc --- /dev/null +++ b/common/src/main/java-templates/me/lucko/luckperms/common/util/BuildInfo.java.peb @@ -0,0 +1,33 @@ +/* + * 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.util; + +public final class BuildInfo { + private BuildInfo() {} + + public static final String VERSION = "{{ version }}"; + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/ActionJsonSerializer.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/ActionJsonSerializer.java index 967219cb3..5874444be 100644 --- a/common/src/main/java/me/lucko/luckperms/common/actionlog/ActionJsonSerializer.java +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/ActionJsonSerializer.java @@ -29,9 +29,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; - import me.lucko.luckperms.common.util.gson.JObject; - import net.luckperms.api.actionlog.Action; import java.time.Instant; diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/Log.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/Log.java deleted file mode 100644 index e72df5d4c..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/actionlog/Log.java +++ /dev/null @@ -1,110 +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.common.actionlog; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSortedSet; - -import me.lucko.luckperms.common.util.ImmutableCollectors; - -import net.luckperms.api.actionlog.Action; - -import java.util.ArrayList; -import java.util.List; -import java.util.SortedSet; -import java.util.UUID; - -public class Log { - private static final Log EMPTY = new Log(ImmutableList.of()); - - public static Builder builder() { - return new Builder(); - } - - public static Log empty() { - return EMPTY; - } - - private final SortedSet content; - - Log(List content) { - this.content = ImmutableSortedSet.copyOf(content); - } - - public SortedSet getContent() { - return this.content; - } - - public SortedSet getContent(UUID actor) { - return this.content.stream() - .filter(e -> e.getSource().getUniqueId().equals(actor)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public SortedSet getUserHistory(UUID uniqueId) { - return this.content.stream() - .filter(e -> e.getTarget().getType() == Action.Target.Type.USER) - .filter(e -> e.getTarget().getUniqueId().isPresent() && e.getTarget().getUniqueId().get().equals(uniqueId)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public SortedSet getGroupHistory(String name) { - return this.content.stream() - .filter(e -> e.getTarget().getType() == Action.Target.Type.GROUP) - .filter(e -> e.getTarget().getName().equals(name)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public SortedSet getTrackHistory(String name) { - return this.content.stream() - .filter(e -> e.getTarget().getType() == Action.Target.Type.TRACK) - .filter(e -> e.getTarget().getName().equals(name)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public SortedSet getSearch(String query) { - return this.content.stream() - .filter(e -> e.matchesSearch(query)) - .collect(ImmutableCollectors.toSortedSet()); - } - - public static class Builder { - private final List content = new ArrayList<>(); - - public Builder add(LoggedAction e) { - this.content.add(e); - return this; - } - - public Log build() { - if (this.content.isEmpty()) { - return EMPTY; - } - return new Log(this.content); - } - } - -} diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/LogDispatcher.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/LogDispatcher.java index dc2c95ac2..befa7ab2a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/actionlog/LogDispatcher.java +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/LogDispatcher.java @@ -29,13 +29,14 @@ import me.lucko.luckperms.common.commands.log.LogNotify; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; - import net.luckperms.api.event.log.LogBroadcastEvent; import net.luckperms.api.event.log.LogNotifyEvent; import java.util.Collection; +import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; public class LogDispatcher { @@ -65,7 +66,12 @@ private boolean shouldBroadcast(LoggedAction entry, LogBroadcastEvent.Origin ori return !this.plugin.getEventDispatcher().dispatchLogBroadcast(cancelled, entry, origin); } - private void broadcast(LoggedAction entry, LogNotifyEvent.Origin origin, Sender sender) { + // broadcast the entry to online players + private void broadcast(LoggedAction entry, LogBroadcastEvent.Origin broadcastOrigin, LogNotifyEvent.Origin origin, Sender sender) { + if (!shouldBroadcast(entry, broadcastOrigin)) { + return; + } + this.plugin.getOnlineSenders() .filter(CommandPermission.LOG_NOTIFY::isAuthorized) .filter(s -> { @@ -75,41 +81,46 @@ private void broadcast(LoggedAction entry, LogNotifyEvent.Origin origin, Sender .forEach(s -> Message.LOG.send(s, entry)); } - public void dispatch(LoggedAction entry, Sender sender) { + // log the entry to storage + public CompletableFuture logToStorage(LoggedAction entry) { if (!this.plugin.getEventDispatcher().dispatchLogPublish(false, entry)) { - this.plugin.getStorage().logAction(entry); + return this.plugin.getStorage().logAction(entry); + } else { + return CompletableFuture.completedFuture(null); } + } - this.plugin.getMessagingService().ifPresent(service -> service.pushLog(entry)); - - if (shouldBroadcast(entry, LogBroadcastEvent.Origin.LOCAL)) { - broadcast(entry, LogNotifyEvent.Origin.LOCAL, sender); + // log the entry to messaging + public CompletableFuture logToMessaging(LoggedAction entry) { + InternalMessagingService messagingService = this.plugin.getMessagingService().orElse(null); + if (messagingService != null) { + return messagingService.pushLog(entry); + } else { + return CompletableFuture.completedFuture(null); } } - public void broadcastFromApi(LoggedAction entry) { - this.plugin.getMessagingService().ifPresent(extendedMessagingService -> extendedMessagingService.pushLog(entry)); + // log the entry to storage and messaging, and broadcast it to online players + private CompletableFuture dispatch(LoggedAction entry, Sender sender, LogBroadcastEvent.Origin broadcastOrigin, LogNotifyEvent.Origin origin) { + CompletableFuture storageFuture = logToStorage(entry); + CompletableFuture messagingFuture = logToMessaging(entry); + broadcast(entry, broadcastOrigin, origin, sender); + return CompletableFuture.allOf(storageFuture, messagingFuture); + } - if (shouldBroadcast(entry, LogBroadcastEvent.Origin.LOCAL_API)) { - broadcast(entry, LogNotifyEvent.Origin.LOCAL_API, null); - } + public CompletableFuture dispatch(LoggedAction entry, Sender sender) { + return dispatch(entry, sender, LogBroadcastEvent.Origin.LOCAL, LogNotifyEvent.Origin.LOCAL); } - public void dispatchFromApi(LoggedAction entry) { - if (!this.plugin.getEventDispatcher().dispatchLogPublish(false, entry)) { - try { - this.plugin.getStorage().logAction(entry).get(); - } catch (Exception e) { - this.plugin.getLogger().warn("Error whilst storing action", e); - } - } + public CompletableFuture dispatchFromApi(LoggedAction entry) { + return dispatch(entry, null, LogBroadcastEvent.Origin.LOCAL_API, LogNotifyEvent.Origin.LOCAL_API); + } - broadcastFromApi(entry); + public void broadcastFromApi(LoggedAction entry) { + broadcast(entry, LogBroadcastEvent.Origin.LOCAL_API, LogNotifyEvent.Origin.LOCAL_API, null); } - public void dispatchFromRemote(LoggedAction entry) { - if (shouldBroadcast(entry, LogBroadcastEvent.Origin.REMOTE)) { - broadcast(entry, LogNotifyEvent.Origin.REMOTE, null); - } + public void broadcastFromRemote(LoggedAction entry) { + broadcast(entry, LogBroadcastEvent.Origin.REMOTE, LogNotifyEvent.Origin.REMOTE, null); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/util/Paginated.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/LogPage.java similarity index 54% rename from common/src/main/java/me/lucko/luckperms/common/util/Paginated.java rename to common/src/main/java/me/lucko/luckperms/common/actionlog/LogPage.java index dd4dcf81a..b9ea7c008 100644 --- a/common/src/main/java/me/lucko/luckperms/common/util/Paginated.java +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/LogPage.java @@ -23,53 +23,50 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.util; +package me.lucko.luckperms.common.actionlog; import com.google.common.collect.ImmutableList; +import me.lucko.luckperms.common.filter.PageParameters; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; -import java.util.Collection; import java.util.List; +import java.util.Objects; -/** - * A simple pagination utility - * - * @param the element type - */ -public class Paginated { - private final List content; +public class LogPage { + public static LogPage of(List content, @Nullable PageParameters params, int totalEntries) { + return new LogPage(content, params, totalEntries); + } - public Paginated(Collection content) { + private final List content; + private final @Nullable PageParameters params; + private final int totalEntries; + + LogPage(List content, @Nullable PageParameters params, int totalEntries) { this.content = ImmutableList.copyOf(content); + this.params = params; + this.totalEntries = totalEntries; } - public List getContent() { + public List getContent() { return this.content; } - public int getMaxPages(int entriesPerPage) { - return (int) Math.ceil((double) this.content.size() / (double) entriesPerPage); - } + public List> getNumberedContent() { + int startIndex = this.params != null + ? this.params.pageSize() * (this.params.pageNumber() - 1) + : 0; - public List> getPage(int pageNo, int pageSize) { - if (pageNo < 1) { - throw new IllegalArgumentException("pageNo cannot be less than 1: " + pageNo); - } - - int first = (pageNo - 1) * pageSize; - if (this.content.size() <= first) { - throw new IllegalStateException("Content does not contain that many elements. (requested page: " + pageNo + - ", page size: " + pageSize + ", page first index: " + first + ", content size: " + this.content.size() + ")"); - } - - int last = first + pageSize - 1; - List> out = new ArrayList<>(pageSize); - - for (int i = first; i <= last && i < this.content.size(); i++) { - out.add(new Entry<>(i + 1, this.content.get(i))); + List> numberedContent = new ArrayList<>(); + for (int i = 0; i < this.content.size(); i++) { + int index = startIndex + i + 1; + numberedContent.add(new Entry<>(index, this.content.get(i))); } + return numberedContent; + } - return out; + public int getTotalEntries() { + return this.totalEntries; } public static final class Entry { @@ -93,6 +90,19 @@ public T value() { public String toString() { return this.position + ": " + this.value; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Entry)) return false; + Entry entry = (Entry) o; + return this.position == entry.position && Objects.equals(this.value, entry.value); + } + + @Override + public int hashCode() { + return Objects.hash(this.position, this.value); + } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/LoggedAction.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/LoggedAction.java index 5c46fe900..00ce2c64d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/actionlog/LoggedAction.java +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/LoggedAction.java @@ -26,7 +26,7 @@ package me.lucko.luckperms.common.actionlog; import com.google.common.base.Strings; - +import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.PermissionHolder; @@ -35,12 +35,10 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.DurationFormatter; - import net.luckperms.api.actionlog.Action; import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; - import org.checkerframework.checker.nullness.qual.NonNull; import java.time.Duration; @@ -50,6 +48,7 @@ import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; /** * An implementation of {@link Action} and {@link Action.Builder}, @@ -125,15 +124,11 @@ public int compareTo(@NonNull Action other) { return ActionComparator.INSTANCE.compare(this, other); } - public boolean matchesSearch(String query) { - query = Objects.requireNonNull(query, "query").toLowerCase(); - return this.source.name.toLowerCase().contains(query) || - this.target.name.toLowerCase().contains(query) || - this.description.toLowerCase().contains(query); - } - public void submit(LuckPermsPlugin plugin, Sender sender) { - plugin.getLogDispatcher().dispatch(this, sender); + CompletableFuture future = plugin.getLogDispatcher().dispatch(this, sender); + if (plugin.getConfiguration().get(ConfigKeys.LOG_SYNCHRONOUSLY_IN_COMMANDS)) { + future.join(); + } } @Override @@ -395,14 +390,14 @@ public Builder description(Object... args) { } } - public static char getTypeCharacter(Target.Type type) { + public static String getTypeString(Target.Type type) { switch (type) { case USER: - return 'U'; + return "U"; case GROUP: - return 'G'; + return "G"; case TRACK: - return 'T'; + return "T"; default: throw new AssertionError(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFields.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFields.java new file mode 100644 index 000000000..b4887e701 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFields.java @@ -0,0 +1,60 @@ +/* + * 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.actionlog.filter; + +import me.lucko.luckperms.common.filter.FilterField; +import net.luckperms.api.actionlog.Action; + +import java.util.UUID; + +public final class ActionFields { + + public static final FilterField SOURCE_UNIQUE_ID = FilterField.named( + "SOURCE_UNIQUE_ID", + action -> action.getSource().getUniqueId() + ); + public static final FilterField SOURCE_NAME = FilterField.named( + "SOURCE_NAME", + action -> action.getSource().getName() + ); + public static final FilterField TARGET_TYPE = FilterField.named( + "TARGET_TYPE", + action -> action.getTarget().getType() + ); + public static final FilterField TARGET_UNIQUE_ID = FilterField.named( + "TARGET_UNIQUE_ID", + action -> action.getTarget().getUniqueId().orElse(null) + ); + public static final FilterField TARGET_NAME = FilterField.named( + "TARGET_NAME", + action -> action.getTarget().getName() + ); + public static final FilterField DESCRIPTION = FilterField.named( + "DESCRIPTION", + action -> action.getDescription() + ); + +} \ No newline at end of file diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterMongoBuilder.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterMongoBuilder.java new file mode 100644 index 000000000..d82ec017e --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterMongoBuilder.java @@ -0,0 +1,70 @@ +/* + * 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.actionlog.filter; + +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.mongo.FilterMongoBuilder; +import net.luckperms.api.actionlog.Action; + +import java.util.UUID; + +public final class ActionFilterMongoBuilder extends FilterMongoBuilder { + public static final ActionFilterMongoBuilder INSTANCE = new ActionFilterMongoBuilder(); + + private ActionFilterMongoBuilder() { + + } + + @Override + public String mapFieldName(FilterField field) { + if (field == ActionFields.SOURCE_UNIQUE_ID) { + return "source.uniqueId"; + } else if (field == ActionFields.SOURCE_NAME) { + return "source.name"; + } else if (field == ActionFields.TARGET_TYPE) { + return "target.type"; + } else if (field == ActionFields.TARGET_UNIQUE_ID) { + return "target.uniqueId"; + } else if (field == ActionFields.TARGET_NAME) { + return "target.name"; + } else if (field == ActionFields.DESCRIPTION) { + return "description"; + } + throw new AssertionError(field); + } + + @Override + public Object mapConstraintValue(Object value) { + if (value instanceof String | value instanceof UUID) { + return value; + } else if (value instanceof Action.Target.Type) { + return ((Action.Target.Type) value).name(); + } else { + throw new IllegalArgumentException("Don't know how to map value with type: " + value.getClass().getName()); + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterSqlBuilder.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterSqlBuilder.java new file mode 100644 index 000000000..e7a71ce9a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilterSqlBuilder.java @@ -0,0 +1,69 @@ +/* + * 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.actionlog.filter; + +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.sql.FilterSqlBuilder; +import net.luckperms.api.actionlog.Action; + +import java.util.UUID; + +public class ActionFilterSqlBuilder extends FilterSqlBuilder { + + @Override + public void visitFieldName(FilterField field) { + if (field == ActionFields.SOURCE_UNIQUE_ID) { + this.builder.append("actor_uuid"); + } else if (field == ActionFields.SOURCE_NAME) { + this.builder.append("actor_name"); + } else if (field == ActionFields.TARGET_TYPE) { + this.builder.append("type"); + } else if (field == ActionFields.TARGET_UNIQUE_ID) { + this.builder.append("acted_uuid"); + } else if (field == ActionFields.TARGET_NAME) { + this.builder.append("acted_name"); + } else if (field == ActionFields.DESCRIPTION) { + this.builder.append("action"); + } else { + throw new AssertionError(field); + } + } + + @Override + public void visitConstraintValue(Object value) { + if (value instanceof String) { + this.builder.variable(((String) value)); + } else if (value instanceof UUID) { + this.builder.variable(value.toString()); + } else if (value instanceof Action.Target.Type) { + this.builder.variable(LoggedAction.getTypeString((Action.Target.Type) value)); + } else { + throw new IllegalArgumentException("Don't know how to write value with type: " + value.getClass().getName()); + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilters.java b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilters.java new file mode 100644 index 000000000..f1513a2ca --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/actionlog/filter/ActionFilters.java @@ -0,0 +1,110 @@ +/* + * 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.actionlog.filter; + +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.ConstraintFactory; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.actionlog.Action.Target; + +import java.util.UUID; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public final class ActionFilters { + private ActionFilters() {} + + // all actions + public static FilterList all() { + return FilterList.empty(); + } + + // all actions performed by a given source (actor) + public static FilterList source(UUID uniqueId) { + return FilterList.and( + ActionFields.SOURCE_UNIQUE_ID.isEqualTo(uniqueId, ConstraintFactory.UUIDS) + ); + } + + // all actions affecting a given user + public static FilterList user(UUID uniqueId) { + return FilterList.and( + ActionFields.TARGET_TYPE.isEqualTo(Target.Type.USER, TARGET_TYPE_CONSTRAINT_FACTORY), + ActionFields.TARGET_UNIQUE_ID.isEqualTo(uniqueId, ConstraintFactory.UUIDS) + ); + } + + // all actions affecting a given group + public static FilterList group(String name) { + return FilterList.and( + ActionFields.TARGET_TYPE.isEqualTo(Target.Type.GROUP, TARGET_TYPE_CONSTRAINT_FACTORY), + ActionFields.TARGET_NAME.isEqualTo(name, ConstraintFactory.STRINGS) + ); + } + + // all actions affecting a given track + public static FilterList track(String name) { + return FilterList.and( + ActionFields.TARGET_TYPE.isEqualTo(Target.Type.TRACK, TARGET_TYPE_CONSTRAINT_FACTORY), + ActionFields.TARGET_NAME.isEqualTo(name, ConstraintFactory.STRINGS) + ); + } + + // all actions matching the given search query + public static FilterList search(String query) { + return FilterList.or( + ActionFields.SOURCE_NAME.isSimilarTo("%" + query + "%", ConstraintFactory.STRINGS), + ActionFields.TARGET_NAME.isSimilarTo("%" + query + "%", ConstraintFactory.STRINGS), + ActionFields.DESCRIPTION.isSimilarTo("%" + query + "%", ConstraintFactory.STRINGS) + ); + } + + private static final ConstraintFactory TARGET_TYPE_CONSTRAINT_FACTORY = new ConstraintFactory() { + @Override + public Predicate equal(Target.Type value) { + return value::equals; + } + + @Override + public Predicate notEqual(Target.Type value) { + return string -> !value.equals(string); + } + + @Override + public Predicate similar(Target.Type value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value.toString()); + return type -> pattern.matcher(type.toString()).matches(); + } + + @Override + public Predicate notSimilar(Target.Type value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value.toString()); + return type -> !pattern.matcher(type.toString()).matches(); + } + }; + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/api/ApiUtils.java b/common/src/main/java/me/lucko/luckperms/common/api/ApiUtils.java index 049606c79..a15bcfaa8 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/ApiUtils.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/ApiUtils.java @@ -26,11 +26,11 @@ package me.lucko.luckperms.common.api; import com.google.common.base.Preconditions; - import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.misc.DataConstraints; +import java.util.Locale; import java.util.function.Predicate; public final class ApiUtils { @@ -55,7 +55,7 @@ public static String checkName(String s) { } Preconditions.checkArgument(DataConstraints.GROUP_NAME_TEST.test(s), "Invalid name entry: " + s); - return s.toLowerCase(); + return s.toLowerCase(Locale.ROOT); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/api/LuckPermsApiProvider.java b/common/src/main/java/me/lucko/luckperms/common/api/LuckPermsApiProvider.java index 77aecbc4f..a68ef671a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/LuckPermsApiProvider.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/LuckPermsApiProvider.java @@ -25,6 +25,7 @@ package me.lucko.luckperms.common.api; +import me.lucko.luckperms.common.api.implementation.ApiActionFilterFactory; import me.lucko.luckperms.common.api.implementation.ApiActionLogger; import me.lucko.luckperms.common.api.implementation.ApiContextManager; import me.lucko.luckperms.common.api.implementation.ApiGroupManager; @@ -41,9 +42,13 @@ import me.lucko.luckperms.common.event.AbstractEventBus; import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; import net.luckperms.api.actionlog.ActionLogger; +import net.luckperms.api.actionlog.filter.ActionFilterFactory; import net.luckperms.api.context.ContextManager; import net.luckperms.api.messaging.MessagingService; import net.luckperms.api.messenger.MessengerProvider; @@ -52,12 +57,12 @@ import net.luckperms.api.model.user.UserManager; import net.luckperms.api.node.NodeBuilderRegistry; import net.luckperms.api.node.matcher.NodeMatcherFactory; +import net.luckperms.api.platform.Health; import net.luckperms.api.platform.Platform; import net.luckperms.api.platform.PlayerAdapter; import net.luckperms.api.platform.PluginMetadata; import net.luckperms.api.query.QueryOptionsRegistry; import net.luckperms.api.track.TrackManager; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; @@ -93,6 +98,37 @@ public LuckPermsApiProvider(LuckPermsPlugin plugin) { this.metaStackFactory = new ApiMetaStackFactory(plugin); } + public void ensureApiWasLoadedByPlugin() { + LuckPermsBootstrap bootstrap = this.plugin.getBootstrap(); + ClassLoader pluginClassLoader; + if (bootstrap instanceof BootstrappedWithLoader) { + pluginClassLoader = ((BootstrappedWithLoader) bootstrap).getLoader().getClass().getClassLoader(); + } else { + pluginClassLoader = bootstrap.getClass().getClassLoader(); + } + + for (Class apiClass : new Class[]{LuckPerms.class, LuckPermsProvider.class}) { + ClassLoader apiClassLoader = apiClass.getClassLoader(); + + if (!apiClassLoader.equals(pluginClassLoader)) { + String guilty = "unknown"; + try { + guilty = bootstrap.identifyClassLoader(apiClassLoader); + } catch (Exception e) { + // ignore + } + + PluginLogger logger = this.plugin.getLogger(); + logger.warn("It seems that the LuckPerms API has been (class)loaded by a plugin other than LuckPerms!"); + logger.warn("The API was loaded by " + apiClassLoader + " (" + guilty + ") and the " + + "LuckPerms plugin was loaded by " + pluginClassLoader.toString() + "."); + logger.warn("This indicates that the other plugin has incorrectly \"shaded\" the " + + "LuckPerms API into its jar file. This can cause errors at runtime and should be fixed."); + return; + } + } + } + @Override public @NonNull String getServerName() { return this.plugin.getConfiguration().get(ConfigKeys.SERVER); @@ -139,6 +175,11 @@ public LuckPermsApiProvider(LuckPermsPlugin plugin) { return this.plugin.getSyncTaskBuffer().request(); } + @Override + public @NonNull Health runHealthCheck() { + return this.plugin.runHealthCheck(); + } + @Override public @NonNull AbstractEventBus getEventBus() { return this.plugin.getEventDispatcher().getEventBus(); @@ -186,4 +227,8 @@ public void registerMessengerProvider(@NonNull MessengerProvider messengerProvid return ApiNodeMatcherFactory.INSTANCE; } + @Override + public @NonNull ActionFilterFactory getActionFilterFactory() { + return ApiActionFilterFactory.INSTANCE; + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilter.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilter.java new file mode 100644 index 000000000..65a4be9ea --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilter.java @@ -0,0 +1,47 @@ +/* + * 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.api.implementation; + +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.actionlog.filter.ActionFilter; + +public class ApiActionFilter implements ActionFilter { + private final FilterList filter; + + public ApiActionFilter(FilterList filter) { + this.filter = filter; + } + + @Override + public boolean test(Action action) { + return this.filter.evaluate(action); + } + + public FilterList getFilter() { + return this.filter; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilterFactory.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilterFactory.java new file mode 100644 index 000000000..5bd9a60c6 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionFilterFactory.java @@ -0,0 +1,77 @@ +/* + * 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.api.implementation; + +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import net.luckperms.api.actionlog.filter.ActionFilter; +import net.luckperms.api.actionlog.filter.ActionFilterFactory; + +import java.util.Objects; +import java.util.UUID; + +public final class ApiActionFilterFactory implements ActionFilterFactory { + public static final ApiActionFilterFactory INSTANCE = new ApiActionFilterFactory(); + + private ApiActionFilterFactory() { + + } + + @Override + public ActionFilter any() { + return new ApiActionFilter(ActionFilters.all()); + } + + @Override + public ActionFilter source(UUID uniqueId) { + Objects.requireNonNull(uniqueId, "uniqueId"); + return new ApiActionFilter(ActionFilters.source(uniqueId)); + } + + @Override + public ActionFilter user(UUID uniqueId) { + Objects.requireNonNull(uniqueId, "uniqueId"); + return new ApiActionFilter(ActionFilters.user(uniqueId)); + } + + @Override + public ActionFilter group(String name) { + Objects.requireNonNull(name, "name"); + return new ApiActionFilter(ActionFilters.group(name)); + } + + @Override + public ActionFilter track(String name) { + Objects.requireNonNull(name, "name"); + return new ApiActionFilter(ActionFilters.track(name)); + } + + @Override + public ActionFilter search(String query) { + Objects.requireNonNull(query, "query"); + return new ApiActionFilter(ActionFilters.search(query)); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLog.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLog.java index 8a45b019e..ef8e7df79 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLog.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLog.java @@ -25,52 +25,63 @@ package me.lucko.luckperms.common.api.implementation; -import me.lucko.luckperms.common.actionlog.Log; -import me.lucko.luckperms.common.api.ApiUtils; - +import com.google.common.collect.ImmutableSortedSet; +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.util.ImmutableCollectors; import net.luckperms.api.actionlog.Action; import net.luckperms.api.actionlog.ActionLog; - import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.List; import java.util.Objects; import java.util.SortedSet; import java.util.UUID; -@SuppressWarnings({"unchecked", "rawtypes"}) +@Deprecated public class ApiActionLog implements ActionLog { - private final Log handle; + private final SortedSet content; - public ApiActionLog(Log handle) { - this.handle = handle; + public ApiActionLog(List content) { + this.content = ImmutableSortedSet.copyOf(content); } @Override public @NonNull SortedSet getContent() { - return (SortedSet) this.handle.getContent(); + return this.content; } @Override public @NonNull SortedSet getContent(@NonNull UUID actor) { Objects.requireNonNull(actor, "actor"); - return (SortedSet) this.handle.getContent(actor); + return this.content.stream() + .filter(e -> e.getSource().getUniqueId().equals(actor)) + .collect(ImmutableCollectors.toSortedSet()); } @Override public @NonNull SortedSet getUserHistory(@NonNull UUID uniqueId) { Objects.requireNonNull(uniqueId, "uuid"); - return (SortedSet) this.handle.getUserHistory(uniqueId); + return this.content.stream() + .filter(e -> e.getTarget().getType() == Action.Target.Type.USER) + .filter(e -> e.getTarget().getUniqueId().isPresent() && e.getTarget().getUniqueId().get().equals(uniqueId)) + .collect(ImmutableCollectors.toSortedSet()); } @Override public @NonNull SortedSet getGroupHistory(@NonNull String name) { Objects.requireNonNull(name, "name"); - return (SortedSet) this.handle.getGroupHistory(ApiUtils.checkName(name)); + return this.content.stream() + .filter(e -> e.getTarget().getType() == Action.Target.Type.GROUP) + .filter(e -> e.getTarget().getName().equals(name)) + .collect(ImmutableCollectors.toSortedSet()); } @Override public @NonNull SortedSet getTrackHistory(@NonNull String name) { Objects.requireNonNull(name, "name"); - return (SortedSet) this.handle.getTrackHistory(ApiUtils.checkName(name)); + return this.content.stream() + .filter(e -> e.getTarget().getType() == Action.Target.Type.TRACK) + .filter(e -> e.getTarget().getName().equals(name)) + .collect(ImmutableCollectors.toSortedSet()); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLogger.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLogger.java index d1167530b..c571d5e04 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLogger.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiActionLogger.java @@ -25,15 +25,22 @@ package me.lucko.luckperms.common.api.implementation; +import me.lucko.luckperms.common.actionlog.LogDispatcher; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.actionlog.Action; import net.luckperms.api.actionlog.ActionLog; import net.luckperms.api.actionlog.ActionLogger; - +import net.luckperms.api.actionlog.filter.ActionFilter; +import net.luckperms.api.util.Page; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; public class ApiActionLogger implements ActionLogger { @@ -49,22 +56,66 @@ public ApiActionLogger(LuckPermsPlugin plugin) { } @Override + @Deprecated public @NonNull CompletableFuture getLog() { - return this.plugin.getStorage().getLog().thenApply(ApiActionLog::new); + return this.plugin.getStorage().getLogPage(ActionFilters.all(), null) + .thenApply(result -> new ApiActionLog(result.getContent())); + } + + @Override + public @NonNull CompletableFuture> queryActions(@NonNull ActionFilter filter) { + return this.plugin.getStorage().getLogPage(getFilterList(filter), null).thenApply(ActionPage::new).thenApply(Page::entries); + } + + @Override + public @NonNull CompletableFuture> queryActions(@NonNull ActionFilter filter, int pageSize, int pageNumber) { + return this.plugin.getStorage().getLogPage(getFilterList(filter), new PageParameters(pageSize, pageNumber)).thenApply(ActionPage::new); } @Override public @NonNull CompletableFuture submit(@NonNull Action entry) { - return CompletableFuture.runAsync(() -> this.plugin.getLogDispatcher().dispatchFromApi((LoggedAction) entry), this.plugin.getBootstrap().getScheduler().async()); + return this.plugin.getLogDispatcher().dispatchFromApi((LoggedAction) entry); } @Override public @NonNull CompletableFuture submitToStorage(@NonNull Action entry) { - return this.plugin.getStorage().logAction(entry); + return this.plugin.getLogDispatcher().logToStorage((LoggedAction) entry); } @Override public @NonNull CompletableFuture broadcastAction(@NonNull Action entry) { - return CompletableFuture.runAsync(() -> this.plugin.getLogDispatcher().broadcastFromApi((LoggedAction) entry), this.plugin.getBootstrap().getScheduler().async()); + LogDispatcher dispatcher = this.plugin.getLogDispatcher(); + + CompletableFuture messagingFuture = dispatcher.logToStorage(((LoggedAction) entry)); + dispatcher.broadcastFromApi(((LoggedAction) entry)); + return messagingFuture; + } + + private static FilterList getFilterList(ActionFilter filter) { + Objects.requireNonNull(filter, "filter"); + if (filter instanceof ApiActionFilter) { + return ((ApiActionFilter) filter).getFilter(); + } else { + throw new IllegalArgumentException("Unknown filter type: " + filter.getClass()); + } + } + + private static final class ActionPage implements Page { + private final LogPage page; + + private ActionPage(LogPage page) { + this.page = page; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public @NonNull List entries() { + return (List) this.page.getContent(); + } + + @Override + public int overallSize() { + return this.page.getTotalEntries(); + } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiContextManager.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiContextManager.java index 58d7bf3d0..227427b88 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiContextManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiContextManager.java @@ -25,17 +25,15 @@ package me.lucko.luckperms.common.api.implementation; -import me.lucko.luckperms.common.context.ContextManager; +import me.lucko.luckperms.common.context.manager.ContextManager; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.query.QueryOptionsBuilderImpl; - import net.luckperms.api.context.ContextCalculator; import net.luckperms.api.context.ContextSetFactory; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.user.User; import net.luckperms.api.query.QueryMode; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiContextSetFactory.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiContextSetFactory.java index 55c78a7c2..e7ec45ad7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiContextSetFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiContextSetFactory.java @@ -25,13 +25,11 @@ package me.lucko.luckperms.common.api.implementation; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; -import me.lucko.luckperms.common.context.contextset.MutableContextSetImpl; - +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.MutableContextSetImpl; import net.luckperms.api.context.ContextSetFactory; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.context.MutableContextSet; - import org.checkerframework.checker.nullness.qual.NonNull; public class ApiContextSetFactory implements ContextSetFactory { diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiGroup.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiGroup.java index 5441d4238..6748455b7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiGroup.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiGroup.java @@ -26,12 +26,9 @@ package me.lucko.luckperms.common.api.implementation; import com.google.common.base.Preconditions; - import me.lucko.luckperms.common.cacheddata.GroupCachedDataManager; import me.lucko.luckperms.common.model.Group; - import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiGroupManager.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiGroupManager.java index d4aeba13b..a118fd248 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiGroupManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiGroupManager.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.api.implementation; import com.google.common.collect.ImmutableListMultimap; - import me.lucko.luckperms.common.api.ApiUtils; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.manager.group.GroupManager; @@ -35,13 +34,11 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.misc.NodeEntry; import me.lucko.luckperms.common.util.ImmutableCollectors; - import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.event.cause.DeletionCause; import net.luckperms.api.node.HeldNode; import net.luckperms.api.node.Node; import net.luckperms.api.node.matcher.NodeMatcher; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Collection; diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMessagingService.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMessagingService.java index 7743edee8..b86624872 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMessagingService.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMessagingService.java @@ -26,10 +26,8 @@ package me.lucko.luckperms.common.api.implementation; import me.lucko.luckperms.common.messaging.InternalMessagingService; - import net.luckperms.api.messaging.MessagingService; import net.luckperms.api.model.user.User; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; @@ -56,4 +54,11 @@ public void pushUserUpdate(@NonNull User user) { Objects.requireNonNull(user, "user"); this.handle.pushUserUpdate(ApiUser.cast(user)); } + + @Override + public void sendCustomMessage(@NonNull String channelId, @NonNull String payload) { + Objects.requireNonNull(channelId, "channelId"); + Objects.requireNonNull(payload, "payload"); + this.handle.pushCustomPayload(channelId, payload); + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMetaStackFactory.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMetaStackFactory.java index 0284d7b74..8abcf2bec 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMetaStackFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiMetaStackFactory.java @@ -26,16 +26,13 @@ package me.lucko.luckperms.common.api.implementation; import com.google.common.collect.ImmutableList; - -import me.lucko.luckperms.common.metastacking.SimpleMetaStackDefinition; -import me.lucko.luckperms.common.metastacking.StandardStackElements; +import me.lucko.luckperms.common.cacheddata.metastack.SimpleMetaStackDefinition; +import me.lucko.luckperms.common.cacheddata.metastack.StandardStackElements; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.metastacking.DuplicateRemovalFunction; import net.luckperms.api.metastacking.MetaStackDefinition; import net.luckperms.api.metastacking.MetaStackElement; import net.luckperms.api.metastacking.MetaStackFactory; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiNodeBuilderRegistry.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiNodeBuilderRegistry.java index 2275c91e6..b11be199f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiNodeBuilderRegistry.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiNodeBuilderRegistry.java @@ -34,7 +34,6 @@ import me.lucko.luckperms.common.node.types.RegexPermission; import me.lucko.luckperms.common.node.types.Suffix; import me.lucko.luckperms.common.node.types.Weight; - import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.NodeBuilderRegistry; import net.luckperms.api.node.types.DisplayNameNode; @@ -45,7 +44,6 @@ import net.luckperms.api.node.types.RegexPermissionNode; import net.luckperms.api.node.types.SuffixNode; import net.luckperms.api.node.types.WeightNode; - import org.checkerframework.checker.nullness.qual.NonNull; public final class ApiNodeBuilderRegistry implements NodeBuilderRegistry { diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiNodeMatcherFactory.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiNodeMatcherFactory.java index e1f6f9dd6..66fa6dcaf 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiNodeMatcherFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiNodeMatcherFactory.java @@ -26,14 +26,12 @@ package me.lucko.luckperms.common.api.implementation; import me.lucko.luckperms.common.node.matcher.StandardNodeMatchers; - import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeEqualityPredicate; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.matcher.NodeMatcher; import net.luckperms.api.node.matcher.NodeMatcherFactory; import net.luckperms.api.node.types.MetaNode; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPermissionHolder.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPermissionHolder.java index 1b9c24838..693a2f6aa 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPermissionHolder.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPermissionHolder.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.util.ImmutableCollectors; - import net.luckperms.api.cacheddata.CachedDataManager; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; @@ -42,7 +41,6 @@ import net.luckperms.api.node.NodeType; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Collection; diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPlatform.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPlatform.java index d19884c76..3dcfcfe1e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPlatform.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPlatform.java @@ -26,10 +26,8 @@ package me.lucko.luckperms.common.api.implementation; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.platform.Platform; import net.luckperms.api.platform.PluginMetadata; - import org.checkerframework.checker.nullness.qual.NonNull; import java.time.Instant; diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPlayerAdapter.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPlayerAdapter.java index bb645f9a6..d093fd9a2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPlayerAdapter.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiPlayerAdapter.java @@ -25,14 +25,12 @@ package me.lucko.luckperms.common.api.implementation; -import me.lucko.luckperms.common.context.ContextManager; +import me.lucko.luckperms.common.context.manager.ContextManager; import me.lucko.luckperms.common.model.manager.user.UserManager; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.user.User; import net.luckperms.api.platform.PlayerAdapter; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; @@ -57,7 +55,9 @@ private P checkType(P player) { public @NonNull User getUser(@NonNull P player) { Objects.requireNonNull(player, "player"); me.lucko.luckperms.common.model.User user = this.userManager.getIfLoaded(this.contextManager.getUniqueId(checkType(player))); - Objects.requireNonNull(user, "user"); + if (user == null) { + throw new IllegalStateException("Unable to get a user for " + player); + } return user.getApiProxy(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiQueryOptionsRegistry.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiQueryOptionsRegistry.java index be41c9c6b..e415eaf20 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiQueryOptionsRegistry.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiQueryOptionsRegistry.java @@ -26,10 +26,8 @@ package me.lucko.luckperms.common.api.implementation; import me.lucko.luckperms.common.query.QueryOptionsImpl; - import net.luckperms.api.query.QueryOptions; import net.luckperms.api.query.QueryOptionsRegistry; - import org.checkerframework.checker.nullness.qual.NonNull; public final class ApiQueryOptionsRegistry implements QueryOptionsRegistry { diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiTrack.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiTrack.java index ad74d7c24..d7836b363 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiTrack.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiTrack.java @@ -26,17 +26,14 @@ package me.lucko.luckperms.common.api.implementation; import com.google.common.base.Preconditions; - import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.ContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.group.Group; import net.luckperms.api.model.user.User; import net.luckperms.api.track.DemotionResult; import net.luckperms.api.track.PromotionResult; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiTrackManager.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiTrackManager.java index 99c062b6a..1d2ce0df7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiTrackManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiTrackManager.java @@ -30,16 +30,15 @@ import me.lucko.luckperms.common.model.manager.track.TrackManager; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.util.ImmutableCollectors; - import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.event.cause.DeletionCause; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; public class ApiTrackManager extends ApiAbstractManager> implements net.luckperms.api.track.TrackManager { public ApiTrackManager(LuckPermsPlugin plugin, TrackManager handle) { @@ -76,6 +75,19 @@ protected net.luckperms.api.track.Track proxy(Track internal) { return this.plugin.getStorage().deleteTrack(ApiTrack.cast(track), DeletionCause.API); } + @Override + public @NonNull CompletableFuture modifyTrack(@NonNull String name, @NonNull Consumer action) { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(action, "action"); + + return this.plugin.getStorage().createAndLoadTrack(name, CreationCause.API) + .thenApplyAsync(track -> { + action.accept(track.getApiProxy()); + return track; + }, this.plugin.getBootstrap().getScheduler().async()) + .thenCompose(track -> this.plugin.getStorage().saveTrack(track)); + } + @Override public @NonNull CompletableFuture loadAllTracks() { return this.plugin.getStorage().loadAllTracks(); diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiUser.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiUser.java index 53ac4f15c..21d62c5dc 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiUser.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiUser.java @@ -26,18 +26,16 @@ package me.lucko.luckperms.common.api.implementation; import com.google.common.base.Preconditions; - import me.lucko.luckperms.common.cacheddata.UserCachedDataManager; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.node.types.Inheritance; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeEqualityPredicate; - import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Locale; import java.util.Objects; import java.util.UUID; @@ -71,7 +69,7 @@ public String getUsername() { @Override public @NonNull String getPrimaryGroup() { - String value = this.handle.getCachedData().getMetaData(this.handle.getQueryOptions()).getPrimaryGroup(MetaCheckEvent.Origin.LUCKPERMS_API); + String value = this.handle.getCachedData().getMetaData(this.handle.getQueryOptions()).getPrimaryGroup(CheckOrigin.LUCKPERMS_API); Objects.requireNonNull(value, "value"); // assert nonnull return value; } @@ -83,11 +81,11 @@ public String getUsername() { return DataMutateResult.FAIL_ALREADY_HAS; } - if (!this.handle.hasNode(DataType.NORMAL, Inheritance.builder(group.toLowerCase()).build(), NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE).asBoolean()) { + if (!this.handle.hasNode(DataType.NORMAL, Inheritance.builder(group.toLowerCase(Locale.ROOT)).build(), NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE).asBoolean()) { return DataMutateResult.FAIL; } - this.handle.getPrimaryGroup().setStoredValue(group.toLowerCase()); + this.handle.getPrimaryGroup().setStoredValue(group.toLowerCase(Locale.ROOT)); return DataMutateResult.SUCCESS; } diff --git a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiUserManager.java b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiUserManager.java index ff1649f69..4103fa1a7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiUserManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/api/implementation/ApiUserManager.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.api.implementation; import com.google.common.collect.ImmutableListMultimap; - import me.lucko.luckperms.common.api.ApiUtils; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.user.UserManager; @@ -35,12 +34,10 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.misc.NodeEntry; import me.lucko.luckperms.common.util.ImmutableCollectors; - import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.HeldNode; import net.luckperms.api.node.Node; import net.luckperms.api.node.matcher.NodeMatcher; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -63,17 +60,37 @@ protected net.luckperms.api.model.user.User proxy(User internal) { return internal == null ? null : internal.getApiProxy(); } + private net.luckperms.api.model.user.User proxyAndRegisterUsage(User internal) { + if (internal != null) { + this.plugin.getUserManager().getHouseKeeper().registerApiUsage(internal.getUniqueId()); + } + return proxy(internal); + } + @Override public @NonNull CompletableFuture loadUser(@NonNull UUID uniqueId, @Nullable String username) { - Objects.requireNonNull(uniqueId, "uuid"); + Objects.requireNonNull(uniqueId, "uniqueId"); ApiUtils.checkUsername(username, this.plugin); - if (this.plugin.getUserManager().getIfLoaded(uniqueId) == null) { - this.plugin.getUserManager().getHouseKeeper().registerApiUsage(uniqueId); + return this.plugin.getStorage().loadUser(uniqueId, username) + .thenApply(this::proxyAndRegisterUsage); + } + + @Override + public @NonNull CompletableFuture> loadUsers(@NonNull Set<@NonNull UUID> uniqueIds) { + Objects.requireNonNull(uniqueIds, "uniqueIds"); + + if (uniqueIds.isEmpty()) { + return CompletableFuture.completedFuture(Map.of()); } - return this.plugin.getStorage().loadUser(uniqueId, username) - .thenApply(this::proxy); + return this.plugin.getStorage().loadUsers(uniqueIds) + .thenApply(map -> map.entrySet().stream() + .collect(ImmutableCollectors.toMap( + Map.Entry::getKey, + entry -> this.proxyAndRegisterUsage(entry.getValue()) + )) + ); } @Override @@ -84,7 +101,7 @@ protected net.luckperms.api.model.user.User proxy(User internal) { @Override public @NonNull CompletableFuture lookupUsername(@NonNull UUID uniqueId) { - Objects.requireNonNull(uniqueId, "uuid"); + Objects.requireNonNull(uniqueId, "uniqueId"); return this.plugin.getStorage().getPlayerName(uniqueId); } @@ -113,7 +130,7 @@ protected net.luckperms.api.model.user.User proxy(User internal) { @Override public @NonNull CompletableFuture savePlayerData(@NonNull UUID uniqueId, @NonNull String username) { - Objects.requireNonNull(uniqueId, "uuid"); + Objects.requireNonNull(uniqueId, "uniqueId"); Objects.requireNonNull(username, "username"); return this.plugin.getStorage().savePlayerData(uniqueId, username); } @@ -153,14 +170,14 @@ protected net.luckperms.api.model.user.User proxy(User internal) { @Override public net.luckperms.api.model.user.User getUser(@NonNull UUID uniqueId) { - Objects.requireNonNull(uniqueId, "uuid"); - return proxy(this.handle.getIfLoaded(uniqueId)); + Objects.requireNonNull(uniqueId, "uniqueId"); + return proxyAndRegisterUsage(this.handle.getIfLoaded(uniqueId)); } @Override public net.luckperms.api.model.user.User getUser(@NonNull String username) { - Objects.requireNonNull(username, "name"); - return proxy(this.handle.getByUsername(username)); + Objects.requireNonNull(username, "username"); + return proxyAndRegisterUsage(this.handle.getByUsername(username)); } @Override @@ -172,7 +189,7 @@ public net.luckperms.api.model.user.User getUser(@NonNull String username) { @Override public boolean isLoaded(@NonNull UUID uniqueId) { - Objects.requireNonNull(uniqueId, "uuid"); + Objects.requireNonNull(uniqueId, "uniqueId"); return this.handle.isLoaded(uniqueId); } diff --git a/common/src/main/java/me/lucko/luckperms/common/backup/Exporter.java b/common/src/main/java/me/lucko/luckperms/common/backup/Exporter.java index b326b7aa9..911e757ab 100644 --- a/common/src/main/java/me/lucko/luckperms/common/backup/Exporter.java +++ b/common/src/main/java/me/lucko/luckperms/common/backup/Exporter.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.backup; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.http.AbstractHttpClient; import me.lucko.luckperms.common.http.UnsuccessfulRequestException; import me.lucko.luckperms.common.locale.Message; @@ -38,10 +37,10 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.Storage; +import me.lucko.luckperms.common.util.CompletableFutures; import me.lucko.luckperms.common.util.gson.GsonProvider; import me.lucko.luckperms.common.util.gson.JArray; import me.lucko.luckperms.common.util.gson.JObject; - import net.kyori.adventure.text.Component; import java.io.BufferedWriter; @@ -193,7 +192,7 @@ private JsonObject exportUsers() { } // all of the threads have been scheduled now and are running. we just need to wait for them all to complete - CompletableFuture overallFuture = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture overallFuture = CompletableFutures.allOf(futures); while (true) { try { @@ -234,7 +233,7 @@ protected void processOutput(JsonObject json) { this.log.log("Finished gathering data, writing file..."); try (BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(Files.newOutputStream(this.filePath)), StandardCharsets.UTF_8))) { - GsonProvider.prettyPrinting().toJson(json, out); + GsonProvider.normal().toJson(json, out); } catch (IOException e) { e.printStackTrace(); } @@ -257,7 +256,7 @@ protected void processOutput(JsonObject json) { ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); try (Writer writer = new OutputStreamWriter(new GZIPOutputStream(bytesOut), StandardCharsets.UTF_8)) { - GsonProvider.prettyPrinting().toJson(json, writer); + GsonProvider.normal().toJson(json, writer); } catch (IOException e) { this.plugin.getLogger().severe("Error compressing data", e); } diff --git a/common/src/main/java/me/lucko/luckperms/common/backup/Importer.java b/common/src/main/java/me/lucko/luckperms/common/backup/Importer.java index dd3a837c1..707a925ea 100644 --- a/common/src/main/java/me/lucko/luckperms/common/backup/Importer.java +++ b/common/src/main/java/me/lucko/luckperms/common/backup/Importer.java @@ -30,7 +30,6 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; @@ -39,8 +38,8 @@ import me.lucko.luckperms.common.node.utils.NodeJsonSerializer; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.CompletableFutures; import me.lucko.luckperms.common.util.Uuids; - import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; @@ -50,6 +49,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -167,7 +167,7 @@ private void parseWebEditorData(Map> groups, Map overallFuture = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture overallFuture = CompletableFutures.allOf(futures); this.notify.forEach(s -> Message.IMPORT_INFO.send(s, "All data entries have been processed and scheduled for import - now waiting for the execution to complete.")); diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdate.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdate.java index 82bf1bae9..5740ccde1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdate.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdate.java @@ -25,16 +25,13 @@ package me.lucko.luckperms.common.bulkupdate; -import me.lucko.luckperms.common.bulkupdate.action.Action; -import me.lucko.luckperms.common.bulkupdate.query.Query; +import me.lucko.luckperms.common.bulkupdate.action.BulkUpdateAction; +import me.lucko.luckperms.common.filter.FilterList; import me.lucko.luckperms.common.model.HolderType; - import net.luckperms.api.node.Node; - import org.checkerframework.checker.nullness.qual.Nullable; import java.util.HashSet; -import java.util.List; import java.util.Objects; import java.util.Set; @@ -48,35 +45,30 @@ public final class BulkUpdate { private final DataType dataType; // the action to apply to the data which matches the constraints - private final Action action; + private final BulkUpdateAction action; - // a set of constraints which data must match to be acted upon - private final List queries; + // a set of filters which data must match to be acted upon + private final FilterList filters; // update statistics of the operation (number of nodes, users and groups affected) private final BulkUpdateStatistics statistics = new BulkUpdateStatistics(); private final boolean trackStatistics; - public BulkUpdate(DataType dataType, Action action, List queries, boolean trackStatistics) { + public BulkUpdate(DataType dataType, BulkUpdateAction action, FilterList filters, boolean trackStatistics) { this.dataType = dataType; this.action = action; - this.queries = queries; + this.filters = filters; this.trackStatistics = trackStatistics; } /** - * Check to see if a Node instance satisfies the constrints of this query + * Check to see if a Node instance satisfies the constraints of this query * * @param node the node to check * @return true if satisfied */ - public boolean satisfiesConstraints(Node node) { - for (Query query : this.queries) { - if (!query.isSatisfiedBy(node)) { - return false; - } - } - return true; + public boolean satisfiesFilters(Node node) { + return this.filters.evaluate(node); } /** @@ -86,7 +78,7 @@ public boolean satisfiesConstraints(Node node) { * @return the transformed node, or null if the node should be deleted */ private Node apply(Node node) { - if (!satisfiesConstraints(node)) { + if (!satisfiesFilters(node)) { return node; // make no change } @@ -131,63 +123,16 @@ private Node apply(Node node) { return results; } - /** - * Converts this {@link BulkUpdate} to SQL syntax - * - * @return this query in SQL form - */ - public PreparedStatementBuilder buildAsSql() { - // DELETE FROM {table} WHERE ... - // UPDATE {table} SET ... WHERE ... - - PreparedStatementBuilder builder = new PreparedStatementBuilder(); - - // add the action - // (DELETE FROM or UPDATE) - this.action.appendSql(builder); - - return appendConstraintsAsSql(builder); - } - - /** - * Appends the constraints of this {@link BulkUpdate} to the provided statement builder in SQL syntax - * - * @param builder the statement builder to append the constraints to - * @return the same statement builder provided as input - */ - public PreparedStatementBuilder appendConstraintsAsSql(PreparedStatementBuilder builder) { - - // if there are no constraints, just return without a WHERE clause - if (this.queries.isEmpty()) { - return builder; - } - - // append constraints - builder.append(" WHERE"); - for (int i = 0; i < this.queries.size(); i++) { - Query query = this.queries.get(i); - - builder.append(" "); - if (i != 0) { - builder.append("AND "); - } - - query.appendSql(builder); - } - - return builder; - } - public DataType getDataType() { return this.dataType; } - public Action getAction() { + public BulkUpdateAction getAction() { return this.action; } - public List getQueries() { - return this.queries; + public FilterList getFilters() { + return this.filters; } public boolean isTrackingStatistics() { @@ -206,12 +151,12 @@ public boolean equals(Object o) { return this.getDataType() == that.getDataType() && Objects.equals(this.getAction(), that.getAction()) && - Objects.equals(this.getQueries(), that.getQueries()); + Objects.equals(this.getFilters(), that.getFilters()); } @Override public int hashCode() { - return Objects.hash(getDataType(), getAction(), getQueries(), isTrackingStatistics()); + return Objects.hash(getDataType(), getAction(), getFilters(), isTrackingStatistics()); } @Override @@ -219,7 +164,7 @@ public String toString() { return "BulkUpdate(" + "dataType=" + this.getDataType() + ", " + "action=" + this.getAction() + ", " + - "constraints=" + this.getQueries() + ", " + + "constraints=" + this.getFilters() + ", " + "trackStatistics=" + this.isTrackingStatistics() + ")"; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateBuilder.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateBuilder.java index e936c28e6..0e7109bd2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateBuilder.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateBuilder.java @@ -26,9 +26,12 @@ package me.lucko.luckperms.common.bulkupdate; import com.google.common.collect.ImmutableList; - -import me.lucko.luckperms.common.bulkupdate.action.Action; -import me.lucko.luckperms.common.bulkupdate.query.Query; +import me.lucko.luckperms.common.bulkupdate.action.BulkUpdateAction; +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.ConstraintFactory; +import me.lucko.luckperms.common.filter.Filter; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.node.Node; import java.util.LinkedHashSet; import java.util.Set; @@ -46,18 +49,18 @@ public static BulkUpdateBuilder create() { private DataType dataType = DataType.ALL; // the action to apply to the data which matches the constraints - private Action action = null; + private BulkUpdateAction action = null; // should the operation count the number of affected nodes, users and groups private boolean trackStatistics = false; - // a set of constraints which data must match to be acted upon - private final Set queries = new LinkedHashSet<>(); + // a set of filters which data must match to be acted upon + private final Set> filters = new LinkedHashSet<>(); private BulkUpdateBuilder() { } - public BulkUpdateBuilder action(Action action) { + public BulkUpdateBuilder action(BulkUpdateAction action) { this.action = action; return this; } @@ -72,8 +75,8 @@ public BulkUpdateBuilder trackStatistics(boolean trackStatistics) { return this; } - public BulkUpdateBuilder query(Query query) { - this.queries.add(query); + public BulkUpdateBuilder filter(BulkUpdateField field, Comparison comparison, String value) { + this.filters.add(new Filter<>(field, ConstraintFactory.STRINGS.build(comparison, value))); return this; } @@ -82,7 +85,8 @@ public BulkUpdate build() { throw new IllegalStateException("no action specified"); } - return new BulkUpdate(this.dataType, this.action, ImmutableList.copyOf(this.queries), this.trackStatistics); + FilterList filters = new FilterList<>(FilterList.LogicalOperator.AND, ImmutableList.copyOf(this.filters)); + return new BulkUpdate(this.dataType, this.action, filters, this.trackStatistics); } @Override @@ -90,7 +94,7 @@ public String toString() { return "BulkUpdateBuilder(" + "dataType=" + this.dataType + ", " + "action=" + this.action + ", " + - "constraints=" + this.queries + ", " + + "constraints=" + this.filters + ", " + "trackStatistics=" + this.trackStatistics + ")"; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateField.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateField.java new file mode 100644 index 000000000..03d674e03 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateField.java @@ -0,0 +1,68 @@ +/* + * 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.bulkupdate; + +import me.lucko.luckperms.common.filter.FilterField; +import net.luckperms.api.context.DefaultContextKeys; +import net.luckperms.api.node.Node; + +import java.util.Locale; + +/** + * Represents a field being used in a bulk update + */ +public enum BulkUpdateField implements FilterField { + + PERMISSION { + @Override + public String getValue(Node node) { + return node.getKey(); + } + }, + + SERVER { + @Override + public String getValue(Node node) { + return node.getContexts().getAnyValue(DefaultContextKeys.SERVER_KEY).orElse("global"); + } + }, + + WORLD { + @Override + public String getValue(Node node) { + return node.getContexts().getAnyValue(DefaultContextKeys.WORLD_KEY).orElse("global"); + } + }; + + public static BulkUpdateField of(String s) { + try { + return valueOf(s.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return null; + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlBuilder.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlBuilder.java new file mode 100644 index 000000000..63b25c200 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlBuilder.java @@ -0,0 +1,76 @@ +/* + * 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.bulkupdate; + +import me.lucko.luckperms.common.bulkupdate.action.BulkUpdateAction; +import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; +import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.sql.FilterSqlBuilder; +import net.luckperms.api.node.Node; + +public class BulkUpdateSqlBuilder extends FilterSqlBuilder { + + public void visit(BulkUpdate update) { + visit(update.getAction()); + visit(update.getFilters()); + } + + public void visit(BulkUpdateAction action) { + if (action instanceof UpdateAction) { + visit(((UpdateAction) action)); + } else if (action instanceof DeleteAction) { + visit(((DeleteAction) action)); + } else { + throw new UnsupportedOperationException(action.getClass().getName()); + } + } + + public void visit(UpdateAction action) { + this.builder.append("UPDATE {table} SET "); + visitFieldName(action.getField()); + this.builder.append("="); + this.builder.variable(action.getNewValue()); + } + + public void visit(DeleteAction action) { + this.builder.append("DELETE FROM {table}"); + } + + @Override + public void visitFieldName(FilterField field) { + if (field == BulkUpdateField.PERMISSION) { + this.builder.append("permission"); + } else if (field == BulkUpdateField.SERVER) { + this.builder.append("server"); + } else if (field == BulkUpdateField.WORLD) { + this.builder.append("world"); + } else { + throw new AssertionError(field); + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/Action.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/BulkUpdateAction.java similarity index 80% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/Action.java rename to common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/BulkUpdateAction.java index 1e25f2d79..946ebccdc 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/Action.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/BulkUpdateAction.java @@ -25,14 +25,12 @@ package me.lucko.luckperms.common.bulkupdate.action; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; - import net.luckperms.api.node.Node; /** * Represents an action to be applied to a given node. */ -public interface Action { +public interface BulkUpdateAction { /** * Gets the name of this action @@ -45,17 +43,8 @@ public interface Action { * Applies this action to the given NodeModel, and returns the result. * * @param from the node to base changes from - * @return the new nodemodel instance, or null if the node should be deleted. + * @return the new node instance, or null if the node should be deleted. */ Node apply(Node from); - /** - * Gets this action in SQL form. - * - * Will include a placeholder for the table, as "{table}". - * - * @param builder the statement builder - */ - void appendSql(PreparedStatementBuilder builder); - } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/DeleteAction.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/DeleteAction.java index c38e97c5f..d5c300c25 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/DeleteAction.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/DeleteAction.java @@ -25,11 +25,9 @@ package me.lucko.luckperms.common.bulkupdate.action; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; - import net.luckperms.api.node.Node; -public class DeleteAction implements Action { +public class DeleteAction implements BulkUpdateAction { public static DeleteAction create() { return new DeleteAction(); @@ -47,9 +45,4 @@ public String getName() { public Node apply(Node from) { return null; // this action just deletes nodes, so return null } - - @Override - public void appendSql(PreparedStatementBuilder builder) { - builder.append("DELETE FROM {table}"); - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/UpdateAction.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/UpdateAction.java index 4fb41e039..89a27f658 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/UpdateAction.java +++ b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/action/UpdateAction.java @@ -25,29 +25,27 @@ package me.lucko.luckperms.common.bulkupdate.action; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; -import me.lucko.luckperms.common.bulkupdate.query.QueryField; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateField; import me.lucko.luckperms.common.node.factory.NodeBuilders; - import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.node.Node; -public class UpdateAction implements Action { +public class UpdateAction implements BulkUpdateAction { - public static UpdateAction of(QueryField field, String value) { + public static UpdateAction of(BulkUpdateField field, String value) { return new UpdateAction(field, value); } // the field we're updating - private final QueryField field; + private final BulkUpdateField field; // the new value of the field - private final String value; + private final String newValue; - private UpdateAction(QueryField field, String value) { + private UpdateAction(BulkUpdateField field, String newValue) { this.field = field; - this.value = value; + this.newValue = newValue; } @Override @@ -55,11 +53,19 @@ public String getName() { return "update"; } + public BulkUpdateField getField() { + return this.field; + } + + public String getNewValue() { + return this.newValue; + } + @Override public Node apply(Node from) { switch (this.field) { case PERMISSION: - return NodeBuilders.determineMostApplicable(this.value) + return NodeBuilders.determineMostApplicable(this.newValue) .value(from.getValue()) .expiry(from.getExpiry()) .context(from.getContexts()) @@ -67,8 +73,8 @@ public Node apply(Node from) { case SERVER: { MutableContextSet contexts = from.getContexts().mutableCopy(); contexts.removeAll(DefaultContextKeys.SERVER_KEY); - if (!this.value.equals("global")) { - contexts.add(DefaultContextKeys.SERVER_KEY, this.value); + if (!this.newValue.equals("global")) { + contexts.add(DefaultContextKeys.SERVER_KEY, this.newValue); } return from.toBuilder() @@ -78,8 +84,8 @@ public Node apply(Node from) { case WORLD: { MutableContextSet contexts = from.getContexts().mutableCopy(); contexts.removeAll(DefaultContextKeys.WORLD_KEY); - if (!this.value.equals("global")) { - contexts.add(DefaultContextKeys.WORLD_KEY, this.value); + if (!this.newValue.equals("global")) { + contexts.add(DefaultContextKeys.WORLD_KEY, this.newValue); } return from.toBuilder() @@ -90,10 +96,4 @@ public Node apply(Node from) { throw new RuntimeException(); } } - - @Override - public void appendSql(PreparedStatementBuilder builder) { - builder.append("UPDATE {table} SET " + this.field.getSqlName() + "="); - builder.variable(this.value); - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/StandardComparison.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/StandardComparison.java deleted file mode 100644 index 3152515e1..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/StandardComparison.java +++ /dev/null @@ -1,112 +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.common.bulkupdate.comparison; - -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; - -import java.util.regex.Pattern; - -/** - * An enumeration of standard {@link Comparison}s. - */ -public enum StandardComparison implements Comparison { - - EQUAL("==", "=") { - @Override - public CompiledExpression compile(String expression) { - return expression::equalsIgnoreCase; - } - }, - - NOT_EQUAL("!=", "!=") { - @Override - public CompiledExpression compile(String expression) { - return string -> !expression.equalsIgnoreCase(string); - } - }, - - SIMILAR("~~", "LIKE") { - @Override - public CompiledExpression compile(String expression) { - Pattern pattern = StandardComparison.compilePatternForLikeSyntax(expression); - return string -> pattern.matcher(string).matches(); - } - }, - - NOT_SIMILAR("!~", "NOT LIKE") { - @Override - public CompiledExpression compile(String expression) { - Pattern pattern = StandardComparison.compilePatternForLikeSyntax(expression); - return string -> !pattern.matcher(string).matches(); - } - }; - - public static final String WILDCARD = "%"; - public static final String WILDCARD_ONE = "_"; - - private final String symbol; - private final String asSql; - - StandardComparison(String symbol, String asSql) { - this.symbol = symbol; - this.asSql = asSql; - } - - @Override - public String getSymbol() { - return this.symbol; - } - - @Override - public void appendSql(PreparedStatementBuilder builder) { - builder.append(this.asSql); - } - - @Override - public String toString() { - return this.symbol; - } - - public static StandardComparison parseComparison(String s) { - for (StandardComparison t : values()) { - if (t.getSymbol().equals(s)) { - return t; - } - } - return null; - } - - static Pattern compilePatternForLikeSyntax(String expression) { - expression = expression.replace(".", "\\."); - - // convert from SQL LIKE syntax to regex - expression = expression.replace(WILDCARD_ONE, "."); - expression = expression.replace(WILDCARD, ".*"); - - return Pattern.compile(expression, Pattern.CASE_INSENSITIVE); - } - -} diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/Query.java b/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/Query.java deleted file mode 100644 index ae37dde50..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/Query.java +++ /dev/null @@ -1,84 +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.common.bulkupdate.query; - -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; - -import net.luckperms.api.context.DefaultContextKeys; -import net.luckperms.api.node.Node; - -/** - * Represents a query component - */ -public class Query { - - public static Query of(QueryField field, Constraint constraint) { - return new Query(field, constraint); - } - - // the field this query is comparing against - private final QueryField field; - - // the constraint - private final Constraint constraint; - - private Query(QueryField field, Constraint constraint) { - this.field = field; - this.constraint = constraint; - } - - /** - * Returns if the given node satisfies this query - * - * @param node the node - * @return true if satisfied - */ - public boolean isSatisfiedBy(Node node) { - switch (this.field) { - case PERMISSION: - return this.constraint.eval(node.getKey()); - case SERVER: - return this.constraint.eval(node.getContexts().getAnyValue(DefaultContextKeys.SERVER_KEY).orElse("global")); - case WORLD: - return this.constraint.eval(node.getContexts().getAnyValue(DefaultContextKeys.WORLD_KEY).orElse("global")); - default: - throw new RuntimeException(); - } - } - - public void appendSql(PreparedStatementBuilder builder) { - this.constraint.appendSql(builder, this.field.getSqlName()); - } - - public QueryField getField() { - return this.field; - } - - public Constraint getConstraint() { - return this.constraint; - } -} 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/cacheddata/AbstractCachedDataManager.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/AbstractCachedDataManager.java index ccf2a0c4a..66e54f300 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/AbstractCachedDataManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/AbstractCachedDataManager.java @@ -27,21 +27,21 @@ import me.lucko.luckperms.common.cache.LoadingMap; import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator; -import me.lucko.luckperms.common.cacheddata.type.MetaCache; +import me.lucko.luckperms.common.cacheddata.type.MonitoredMetaCache; import me.lucko.luckperms.common.cacheddata.type.PermissionCache; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.calculator.PermissionCalculator; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.util.CaffeineFactory; - +import me.lucko.luckperms.common.util.CompletableFutures; import net.luckperms.api.cacheddata.CachedData; import net.luckperms.api.cacheddata.CachedDataManager; import net.luckperms.api.cacheddata.CachedMetaData; import net.luckperms.api.cacheddata.CachedPermissionData; import net.luckperms.api.metastacking.MetaStackDefinition; import net.luckperms.api.node.ChatMetaType; +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Map; @@ -59,7 +59,7 @@ public abstract class AbstractCachedDataManager implements CachedDataManager { private final LuckPermsPlugin plugin; private final AbstractContainer permission; - private final AbstractContainer meta; + private final AbstractContainer meta; protected AbstractCachedDataManager(LuckPermsPlugin plugin) { this.plugin = plugin; @@ -87,7 +87,7 @@ public LuckPermsPlugin getPlugin() { } @Override - public @NonNull MetaCache getMetaData(@NonNull QueryOptions queryOptions) { + public @NonNull MonitoredMetaCache getMetaData(@NonNull QueryOptions queryOptions) { return this.meta.get(queryOptions); } @@ -97,7 +97,7 @@ public LuckPermsPlugin getPlugin() { } @Override - public @NonNull MetaCache getMetaData() { + public @NonNull MonitoredMetaCache getMetaData() { return getMetaData(getQueryOptions()); } @@ -139,7 +139,7 @@ public LuckPermsPlugin getPlugin() { * @param the map type * @return the resolved permissions */ - protected abstract > M resolvePermissions(IntFunction mapFactory, QueryOptions queryOptions); + protected abstract > M resolvePermissions(IntFunction mapFactory, QueryOptions queryOptions); /** * Resolves the owners meta data for the given {@link QueryOptions}. @@ -153,18 +153,18 @@ private PermissionCache calculatePermissions(QueryOptions queryOptions) { Objects.requireNonNull(queryOptions, "queryOptions"); CacheMetadata metadata = getMetadataForQueryOptions(queryOptions); - ConcurrentHashMap sourcePermissions = resolvePermissions(ConcurrentHashMap::new, queryOptions); + ConcurrentHashMap sourcePermissions = resolvePermissions(ConcurrentHashMap::new, queryOptions); return new PermissionCache(queryOptions, metadata, getCalculatorFactory(), sourcePermissions); } - private MetaCache calculateMeta(QueryOptions queryOptions) { + private MonitoredMetaCache calculateMeta(QueryOptions queryOptions) { Objects.requireNonNull(queryOptions, "queryOptions"); CacheMetadata metadata = getMetadataForQueryOptions(queryOptions); MetaAccumulator accumulator = newAccumulator(queryOptions); resolveMeta(accumulator, queryOptions); - return new MetaCache(this.plugin, queryOptions, metadata, accumulator); + return new MonitoredMetaCache(this.plugin, queryOptions, metadata, accumulator); } @Override @@ -193,7 +193,7 @@ public AbstractContainer(Function cacheLoader) { } public void cleanup() { - this.cache.values().removeIf(value -> ((UsageTracked) value).usedSince(TimeUnit.MINUTES.toMillis(2))); + this.cache.values().removeIf(value -> !((UsageTracked) value).usedInTheLast(2, TimeUnit.MINUTES)); } @Override @@ -240,7 +240,7 @@ public void recalculate() { @Override public @NonNull CompletableFuture reload() { Set keys = this.cache.keySet(); - return CompletableFuture.allOf(keys.stream().map(this::reload).toArray(CompletableFuture[]::new)); + return CompletableFutures.allOf(keys.stream().map(this::reload)); } @Override @@ -254,24 +254,11 @@ public void invalidate() { this.cache.clear(); } } - - private MetaStackDefinition getMetaStackDefinition(QueryOptions queryOptions, ChatMetaType type) { - MetaStackDefinition stack = queryOptions.option(type == ChatMetaType.PREFIX ? - MetaStackDefinition.PREFIX_STACK_KEY : - MetaStackDefinition.SUFFIX_STACK_KEY - ).orElse(null); - - if (stack == null) { - stack = getDefaultMetaStackDefinition(type); - } - - return stack; - } private MetaAccumulator newAccumulator(QueryOptions queryOptions) { return new MetaAccumulator( - getMetaStackDefinition(queryOptions, ChatMetaType.PREFIX), - getMetaStackDefinition(queryOptions, ChatMetaType.SUFFIX) + queryOptions.option(MetaStackDefinition.PREFIX_STACK_KEY).orElseGet(() -> getDefaultMetaStackDefinition(ChatMetaType.PREFIX)), + queryOptions.option(MetaStackDefinition.SUFFIX_STACK_KEY).orElseGet(() -> getDefaultMetaStackDefinition(ChatMetaType.SUFFIX)) ); } diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/CacheMetadata.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/CacheMetadata.java index 84c9095ac..3ca24a162 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/CacheMetadata.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/CacheMetadata.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; - import net.luckperms.api.cacheddata.CachedData; import net.luckperms.api.query.QueryOptions; diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/GroupCachedDataManager.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/GroupCachedDataManager.java index eae5955ad..a6c660f37 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/GroupCachedDataManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/GroupCachedDataManager.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; - import net.luckperms.api.cacheddata.CachedDataManager; import net.luckperms.api.query.QueryOptions; diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/HolderCachedDataManager.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/HolderCachedDataManager.java index 6938e7651..152126fce 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/HolderCachedDataManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/HolderCachedDataManager.java @@ -29,9 +29,9 @@ import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.model.PermissionHolder; - import net.luckperms.api.metastacking.MetaStackDefinition; import net.luckperms.api.node.ChatMetaType; +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; import java.util.Map; @@ -75,7 +75,7 @@ protected MetaStackDefinition getDefaultMetaStackDefinition(ChatMetaType type) { } @Override - protected > M resolvePermissions(IntFunction mapFactory, QueryOptions queryOptions) { + protected > M resolvePermissions(IntFunction mapFactory, QueryOptions queryOptions) { return this.holder.exportPermissions(mapFactory, queryOptions, true, getPlugin().getConfiguration().get(ConfigKeys.APPLYING_SHORTHAND)); } diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/UsageTracked.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/UsageTracked.java index 602d9cacd..250130ddb 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/UsageTracked.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/UsageTracked.java @@ -25,14 +25,20 @@ package me.lucko.luckperms.common.cacheddata; +import com.google.common.annotations.VisibleForTesting; + +import java.util.concurrent.TimeUnit; + public abstract class UsageTracked { - private long lastUsed = System.currentTimeMillis(); + + @VisibleForTesting + protected long lastUsed = System.currentTimeMillis(); public void recordUsage() { this.lastUsed = System.currentTimeMillis(); } - public boolean usedSince(long duration) { - return this.lastUsed > System.currentTimeMillis() - duration; + public boolean usedInTheLast(long duration, TimeUnit unit) { + return this.lastUsed > System.currentTimeMillis() - unit.toMillis(duration); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/UserCachedDataManager.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/UserCachedDataManager.java index 36b132d1c..f22fe7ae8 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/UserCachedDataManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/UserCachedDataManager.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; - import net.luckperms.api.cacheddata.CachedDataManager; import net.luckperms.api.query.QueryOptions; diff --git a/common/src/main/java/me/lucko/luckperms/common/metastacking/FluentMetaStackElement.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/metastack/FluentMetaStackElement.java similarity index 98% rename from common/src/main/java/me/lucko/luckperms/common/metastacking/FluentMetaStackElement.java rename to common/src/main/java/me/lucko/luckperms/common/cacheddata/metastack/FluentMetaStackElement.java index cd6ebbb6f..c7d5ce48c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/metastacking/FluentMetaStackElement.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/metastack/FluentMetaStackElement.java @@ -23,15 +23,13 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.metastacking; +package me.lucko.luckperms.common.cacheddata.metastack; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; - import net.luckperms.api.metastacking.MetaStackElement; import net.luckperms.api.node.ChatMetaType; import net.luckperms.api.node.types.ChatMetaNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/metastacking/SimpleMetaStackDefinition.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/metastack/SimpleMetaStackDefinition.java similarity index 98% rename from common/src/main/java/me/lucko/luckperms/common/metastacking/SimpleMetaStackDefinition.java rename to common/src/main/java/me/lucko/luckperms/common/cacheddata/metastack/SimpleMetaStackDefinition.java index 07d23c097..bda2e9265 100644 --- a/common/src/main/java/me/lucko/luckperms/common/metastacking/SimpleMetaStackDefinition.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/metastack/SimpleMetaStackDefinition.java @@ -23,14 +23,12 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.metastacking; +package me.lucko.luckperms.common.cacheddata.metastack; import com.google.common.collect.ImmutableList; - import net.luckperms.api.metastacking.DuplicateRemovalFunction; import net.luckperms.api.metastacking.MetaStackDefinition; import net.luckperms.api.metastacking.MetaStackElement; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; diff --git a/common/src/main/java/me/lucko/luckperms/common/metastacking/StandardStackElements.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/metastack/StandardStackElements.java similarity index 95% rename from common/src/main/java/me/lucko/luckperms/common/metastacking/StandardStackElements.java rename to common/src/main/java/me/lucko/luckperms/common/cacheddata/metastack/StandardStackElements.java index 0c94af9cd..a67ee0c87 100644 --- a/common/src/main/java/me/lucko/luckperms/common/metastacking/StandardStackElements.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/metastack/StandardStackElements.java @@ -23,22 +23,21 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.metastacking; +package me.lucko.luckperms.common.cacheddata.metastack; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.util.ImmutableCollectors; - import net.luckperms.api.metastacking.MetaStackElement; import net.luckperms.api.model.PermissionHolder; import net.luckperms.api.node.ChatMetaType; import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; import net.luckperms.api.node.types.ChatMetaNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.List; +import java.util.Locale; import java.util.Objects; /** @@ -48,7 +47,7 @@ public final class StandardStackElements { private StandardStackElements() {} public static MetaStackElement parseFromString(LuckPermsPlugin plugin, String s) { - s = s.toLowerCase(); + s = s.toLowerCase(Locale.ROOT); // static if (s.equals("highest")) return HIGHEST; @@ -221,8 +220,11 @@ private static final class FromGroupOnTrackCheck implements MetaStackElement { @Override public boolean shouldAccumulate(@NonNull ChatMetaType type, @NonNull ChatMetaNode node, @Nullable ChatMetaNode current) { Track track = this.plugin.getTrackManager().getIfLoaded(this.trackName); + if (track == null) { + return false; + } PermissionHolder.Identifier origin = node.metadata(InheritanceOriginMetadata.KEY).getOrigin(); - return track != null && origin.getType().equals(PermissionHolder.Identifier.GROUP_TYPE) && track.containsGroup(origin.getName()); + return origin.getType().equals(PermissionHolder.Identifier.GROUP_TYPE) && track.containsGroup(origin.getName()); } @Override @@ -251,8 +253,11 @@ private static final class NotFromGroupOnTrackCheck implements MetaStackElement @Override public boolean shouldAccumulate(@NonNull ChatMetaType type, @NonNull ChatMetaNode node, @Nullable ChatMetaNode current) { Track track = this.plugin.getTrackManager().getIfLoaded(this.trackName); + if (track == null) { + return false; + } PermissionHolder.Identifier origin = node.metadata(InheritanceOriginMetadata.KEY).getOrigin(); - return track != null && !track.containsGroup(origin.getName()); + return !(origin.getType().equals(PermissionHolder.Identifier.GROUP_TYPE) && track.containsGroup(origin.getName())); } @Override @@ -306,7 +311,7 @@ private static final class NotFromGroupCheck implements MetaStackElement { @Override public boolean shouldAccumulate(@NonNull ChatMetaType type, @NonNull ChatMetaNode node, @Nullable ChatMetaNode current) { PermissionHolder.Identifier origin = node.metadata(InheritanceOriginMetadata.KEY).getOrigin(); - return !this.groupName.equals(origin.getName()); + return !(origin.getType().equals(PermissionHolder.Identifier.GROUP_TYPE) && this.groupName.equals(origin.getName())); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/AbstractResult.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/AbstractResult.java new file mode 100644 index 000000000..5d5199de2 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/AbstractResult.java @@ -0,0 +1,57 @@ +/* + * 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.cacheddata.result; + +import net.luckperms.api.cacheddata.Result; +import net.luckperms.api.node.Node; +import org.checkerframework.checker.nullness.qual.Nullable; + +public abstract class AbstractResult> implements Result { + + /** The node that caused the result */ + protected final N node; + /** A reference to another result that this one overrides */ + protected S overriddenResult; + + public AbstractResult(N node, S overriddenResult) { + this.node = node; + this.overriddenResult = overriddenResult; + } + + @Override + public final @Nullable N node() { + return this.node; + } + + public final @Nullable S overriddenResult() { + return this.overriddenResult; + } + + public final void setOverriddenResult(S overriddenResult) { + this.overriddenResult = overriddenResult; + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/IntegerResult.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/IntegerResult.java new file mode 100644 index 000000000..e7230b331 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/IntegerResult.java @@ -0,0 +1,104 @@ +/* + * 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.cacheddata.result; + +import net.luckperms.api.node.Node; +import net.luckperms.api.node.types.WeightNode; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents the result of an integer meta lookup + * + * @param the node type + */ +public final class IntegerResult extends AbstractResult> { + + /** The result */ + private final int result; + + public IntegerResult(int result, N node, IntegerResult overriddenResult) { + super(node, overriddenResult); + this.result = result; + } + + @Override + @Deprecated // use intResult() + public @NonNull Integer result() { + return this.result; + } + + public int intResult() { + return this.result; + } + + public StringResult asStringResult() { + if (isNull()) { + return StringResult.nullResult(); + } else { + StringResult result = StringResult.of(Integer.toString(this.result), this.node); + if (this.overriddenResult != null) { + result.setOverriddenResult(this.overriddenResult.asStringResult()); + } + return result; + } + } + + public boolean isNull() { + return this == NULL_RESULT; + } + + public IntegerResult copy() { + return new IntegerResult<>(this.result, this.node, this.overriddenResult); + } + + @Override + public String toString() { + return "IntegerResult(" + + "result=" + this.result + ", " + + "node=" + this.node + ", " + + "overriddenResult=" + this.overriddenResult + ')'; + } + + private static final IntegerResult NULL_RESULT = new IntegerResult<>(0, null, null); + + @SuppressWarnings("unchecked") + public static IntegerResult nullResult() { + return (IntegerResult) NULL_RESULT; + } + + public static IntegerResult of(int result) { + return new IntegerResult<>(result, null, null); + } + + public static IntegerResult of(int result, N node) { + return new IntegerResult<>(result, node, null); + } + + public static IntegerResult of(WeightNode node) { + return new IntegerResult<>(node.getWeight(), node, null); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/StringResult.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/StringResult.java new file mode 100644 index 000000000..204117608 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/StringResult.java @@ -0,0 +1,88 @@ +/* + * 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.cacheddata.result; + +import net.luckperms.api.node.Node; +import net.luckperms.api.node.types.ChatMetaNode; +import net.luckperms.api.node.types.MetaNode; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Represents the result of a meta lookup + * + * @param the node type + */ +public final class StringResult extends AbstractResult> { + + /** The result, nullable */ + private final String result; + + public StringResult(String result, N node, StringResult overriddenResult) { + super(node, overriddenResult); + this.result = result; + } + + @Override + public @Nullable String result() { + return this.result; + } + + public StringResult copy() { + return new StringResult<>(this.result, this.node, this.overriddenResult); + } + + @Override + public String toString() { + return "StringResult(" + + "result=" + this.result + ", " + + "node=" + this.node + ", " + + "overriddenResult=" + this.overriddenResult + ')'; + } + + private static final StringResult NULL_RESULT = new StringResult<>(null, null, null); + + @SuppressWarnings("unchecked") + public static StringResult nullResult() { + return (StringResult) NULL_RESULT; + } + + public static StringResult of(String result) { + return new StringResult<>(result, null, null); + } + + public static StringResult of(String result, N node) { + return new StringResult<>(result, node, null); + } + + public static StringResult of(MetaNode node) { + return new StringResult<>(node.getMetaValue(), node, null); + } + + public static > StringResult of(N node) { + return new StringResult<>(node.getMetaValue(), node, null); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/result/TristateResult.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/TristateResult.java similarity index 52% rename from common/src/main/java/me/lucko/luckperms/common/calculator/result/TristateResult.java rename to common/src/main/java/me/lucko/luckperms/common/cacheddata/result/TristateResult.java index 931b801a7..1b0dcc019 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculator/result/TristateResult.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/result/TristateResult.java @@ -23,78 +23,90 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.calculator.result; +package me.lucko.luckperms.common.cacheddata.result; import me.lucko.luckperms.common.calculator.PermissionCalculator; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; - +import net.luckperms.api.node.Node; import net.luckperms.api.util.Tristate; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Represents the result of a {@link PermissionCalculator} lookup. */ -public final class TristateResult { - - private static final Factory NULL_FACTORY = new Factory(null, null); - public static final TristateResult UNDEFINED = new TristateResult(Tristate.UNDEFINED, null, null); +public final class TristateResult extends AbstractResult { - public static TristateResult of(Tristate result) { - return NULL_FACTORY.result(result); - } - + /** The result */ private final Tristate result; + /** The permission processor that provided the result */ private final Class processorClass; - private final String cause; - private TristateResult(Tristate result, Class processorClass, String cause) { + private TristateResult(Tristate result, Node node, Class processorClass) { + super(node, null); this.result = result; this.processorClass = processorClass; - this.cause = cause; } - public Tristate result() { + @Override + public @NonNull Tristate result() { return this.result; } - public Class processorClass() { + public @Nullable Class processorClass() { return this.processorClass; } - public String cause() { - return this.cause; + public @Nullable String processorClassFriendly() { + if (this.processorClass == null) { + return null; + } else if (this.processorClass.getName().startsWith("me.lucko.luckperms.")) { + String simpleName = this.processorClass.getSimpleName(); + String platform = this.processorClass.getName().split("\\.")[3]; + return platform + "." + simpleName; + } else { + return this.processorClass.getName(); + } } @Override public String toString() { return "TristateResult(" + "result=" + this.result + ", " + + "node=" + this.node + ", " + "processorClass=" + this.processorClass + ", " + - "cause=" + this.cause + ')'; + "overriddenResult=" + this.overriddenResult + ')'; + } + + private static final TristateResult TRUE = new TristateResult(Tristate.TRUE, null,null); + private static final TristateResult FALSE = new TristateResult(Tristate.FALSE, null,null); + public static final TristateResult UNDEFINED = new TristateResult(Tristate.UNDEFINED, null,null); + + public static TristateResult forMonitoredResult(Tristate result) { + switch (result) { + case TRUE: + return TRUE; + case FALSE: + return FALSE; + case UNDEFINED: + return UNDEFINED; + default: + throw new AssertionError(); + } } public static final class Factory { private final Class processorClass; - private final TristateResult trueResult; - private final TristateResult falseResult; - - public Factory(Class processorClass, String defaultCause) { - this.processorClass = processorClass; - - this.trueResult = new TristateResult(Tristate.TRUE, processorClass, defaultCause); - this.falseResult = new TristateResult(Tristate.FALSE, processorClass, defaultCause); - } - public Factory(Class processorClass) { - this(processorClass, null); + this.processorClass = processorClass; } public TristateResult result(Tristate result) { switch (result) { case TRUE: - return this.trueResult; case FALSE: - return this.falseResult; + return new TristateResult(result, null, this.processorClass); case UNDEFINED: return UNDEFINED; default: @@ -102,11 +114,22 @@ public TristateResult result(Tristate result) { } } - public TristateResult result(Tristate result, String cause) { + public TristateResult result(@Nullable Node node) { + if (node == null) { + return UNDEFINED; + } + return new TristateResult(Tristate.of(node.getValue()), node, this.processorClass); + } + + public TristateResult resultWithOverride(@Nullable Node node, @NonNull Tristate result) { if (result == Tristate.UNDEFINED) { return UNDEFINED; } - return new TristateResult(result, this.processorClass, cause); + return new TristateResult(result, node, this.processorClass); + } + + public TristateResult result(TristateResult result) { + return new TristateResult(result.result(), result.node(), this.processorClass); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaAccumulator.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaAccumulator.java index e449ae0e4..008c6fd21 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaAccumulator.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaAccumulator.java @@ -27,21 +27,25 @@ import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; - +import me.lucko.luckperms.common.cacheddata.result.IntegerResult; +import me.lucko.luckperms.common.cacheddata.result.StringResult; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.node.types.Weight; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.metastacking.MetaStackDefinition; import net.luckperms.api.node.ChatMetaType; import net.luckperms.api.node.Node; +import net.luckperms.api.node.types.ChatMetaNode; import net.luckperms.api.node.types.MetaNode; import net.luckperms.api.node.types.PrefixNode; import net.luckperms.api.node.types.SuffixNode; +import net.luckperms.api.node.types.WeightNode; import java.util.Comparator; +import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicReference; @@ -73,16 +77,18 @@ private enum State { private final AtomicReference state = new AtomicReference<>(State.ACCUMULATING); - private final ListMultimap meta; - private final SortedMap prefixes; - private final SortedMap suffixes; - private int weight = 0; + private final ListMultimap> meta; + private final SortedMap> prefixes; + private final SortedMap> suffixes; + private IntegerResult weight; private String primaryGroup; + private Set seenNodeKeys = new HashSet<>(); + private final MetaStackDefinition prefixDefinition; private final MetaStackDefinition suffixDefinition; - private final MetaStackAccumulator prefixAccumulator; - private final MetaStackAccumulator suffixAccumulator; + private final MetaStackAccumulator prefixAccumulator; + private final MetaStackAccumulator suffixAccumulator; public MetaAccumulator(MetaStackDefinition prefixDefinition, MetaStackDefinition suffixDefinition) { Objects.requireNonNull(prefixDefinition, "prefixDefinition"); @@ -90,10 +96,11 @@ public MetaAccumulator(MetaStackDefinition prefixDefinition, MetaStackDefinition this.meta = ArrayListMultimap.create(); this.prefixes = new TreeMap<>(Comparator.reverseOrder()); this.suffixes = new TreeMap<>(Comparator.reverseOrder()); + this.weight = IntegerResult.nullResult(); this.prefixDefinition = prefixDefinition; this.suffixDefinition = suffixDefinition; - this.prefixAccumulator = new MetaStackAccumulator(this.prefixDefinition, ChatMetaType.PREFIX); - this.suffixAccumulator = new MetaStackAccumulator(this.suffixDefinition, ChatMetaType.SUFFIX); + this.prefixAccumulator = new MetaStackAccumulator<>(this.prefixDefinition, ChatMetaType.PREFIX); + this.suffixAccumulator = new MetaStackAccumulator<>(this.suffixDefinition, ChatMetaType.SUFFIX); } private void ensureState(State state) { @@ -114,12 +121,13 @@ public void complete() { } // perform final changes - if (!this.meta.containsKey(Weight.NODE_KEY) && this.weight != 0) { - this.meta.put(Weight.NODE_KEY, String.valueOf(this.weight)); + if (!this.meta.containsKey(Weight.NODE_KEY) && !this.weight.isNull()) { + this.meta.put(Weight.NODE_KEY, StringResult.of(String.valueOf(this.weight.intResult()))); } if (this.primaryGroup != null && !this.meta.containsKey("primarygroup")) { - this.meta.put("primarygroup", this.primaryGroup); + this.meta.put("primarygroup", StringResult.of(this.primaryGroup)); } + this.seenNodeKeys = null; // free up for GC this.state.set(State.COMPLETE); } @@ -129,32 +137,39 @@ public void complete() { public void accumulateNode(Node n) { ensureState(State.ACCUMULATING); + // only process distinct nodes once, allows inheritance to be + // "cancelled out" by assigning a false copy. + if (!this.seenNodeKeys.add(n.getKey())) { + return; + } + + if (!n.getValue()) { + return; + } + if (n instanceof MetaNode) { MetaNode mn = (MetaNode) n; - this.meta.put(mn.getMetaKey(), mn.getMetaValue()); + this.meta.put(mn.getMetaKey(), StringResult.of(mn)); } if (n instanceof PrefixNode) { PrefixNode pn = (PrefixNode) n; - this.prefixes.putIfAbsent(pn.getPriority(), pn.getMetaValue()); + this.prefixes.putIfAbsent(pn.getPriority(), StringResult.of(pn)); this.prefixAccumulator.offer(pn); } if (n instanceof SuffixNode) { SuffixNode pn = (SuffixNode) n; - this.suffixes.putIfAbsent(pn.getPriority(), pn.getMetaValue()); + this.suffixes.putIfAbsent(pn.getPriority(), StringResult.of(pn)); this.suffixAccumulator.offer(pn); } } - public void accumulateMeta(String key, String value) { - ensureState(State.ACCUMULATING); - this.meta.put(key, value); - } - - public void accumulateWeight(int weight) { + public void accumulateWeight(IntegerResult weight) { ensureState(State.ACCUMULATING); - this.weight = Math.max(this.weight, weight); + if (this.weight.isNull() || weight.intResult() > this.weight.intResult()) { + this.weight = weight; + } } public void setPrimaryGroup(String primaryGroup) { @@ -164,27 +179,27 @@ public void setPrimaryGroup(String primaryGroup) { // read methods - public ListMultimap getMeta() { + public ListMultimap> getMeta() { ensureState(State.COMPLETE); return this.meta; } - public Map getChatMeta(ChatMetaType type) { + public Map>> getChatMeta(ChatMetaType type) { ensureState(State.COMPLETE); return type == ChatMetaType.PREFIX ? this.prefixes : this.suffixes; } - public SortedMap getPrefixes() { + public SortedMap> getPrefixes() { ensureState(State.COMPLETE); return this.prefixes; } - public SortedMap getSuffixes() { + public SortedMap> getSuffixes() { ensureState(State.COMPLETE); return this.suffixes; } - public int getWeight() { + public IntegerResult getWeight() { ensureState(State.COMPLETE); return this.weight; } @@ -204,14 +219,14 @@ public MetaStackDefinition getSuffixDefinition() { return this.suffixDefinition; } - public String getPrefix() { + public StringResult getPrefix() { ensureState(State.COMPLETE); - return this.prefixAccumulator.toFormattedString(); + return this.prefixAccumulator.toResult(); } - public String getSuffix() { + public StringResult getSuffix() { ensureState(State.COMPLETE); - return this.suffixAccumulator.toFormattedString(); + return this.suffixAccumulator.toResult(); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaCache.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaCache.java index f65b7678a..46670715f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaCache.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaCache.java @@ -26,102 +26,209 @@ package me.lucko.luckperms.common.cacheddata.type; import com.google.common.collect.ForwardingMap; - -import me.lucko.luckperms.common.cacheddata.CacheMetadata; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimaps; +import me.lucko.luckperms.common.cacheddata.UsageTracked; +import me.lucko.luckperms.common.cacheddata.result.IntegerResult; +import me.lucko.luckperms.common.cacheddata.result.StringResult; +import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.node.types.Prefix; import me.lucko.luckperms.common.node.types.Suffix; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.cacheddata.CachedMetaData; +import net.luckperms.api.cacheddata.Result; +import net.luckperms.api.metastacking.MetaStackDefinition; +import net.luckperms.api.node.types.MetaNode; +import net.luckperms.api.node.types.PrefixNode; +import net.luckperms.api.node.types.SuffixNode; +import net.luckperms.api.node.types.WeightNode; import net.luckperms.api.query.QueryOptions; - +import net.luckperms.api.query.meta.MetaValueSelector; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Objects; +import java.util.SortedMap; /** * Holds cached meta for a given context */ -public class MetaCache extends SimpleMetaCache implements CachedMetaData { +public class MetaCache extends UsageTracked implements CachedMetaData { - /** The plugin instance */ private final LuckPermsPlugin plugin; - /** The metadata for this cache */ - private final CacheMetadata metadata; - - public MetaCache(LuckPermsPlugin plugin, QueryOptions queryOptions, CacheMetadata metadata, MetaAccumulator sourceMeta) { - super(plugin, queryOptions, sourceMeta); + /** The query options this container is holding data for */ + private final QueryOptions queryOptions; + + /* The data */ + private final Map>> meta; + private final Map> flattenedMeta; + private final SortedMap> prefixes; + private final SortedMap> suffixes; + private final IntegerResult weight; + private final String primaryGroup; + private final MetaStackDefinition prefixDefinition; + private final MetaStackDefinition suffixDefinition; + private final StringResult prefix; + private final StringResult suffix; + + public MetaCache(LuckPermsPlugin plugin, QueryOptions queryOptions, MetaAccumulator sourceMeta) { this.plugin = plugin; - this.metadata = metadata; + this.queryOptions = queryOptions; + + Map>> meta = Multimaps.asMap(ImmutableListMultimap.copyOf(sourceMeta.getMeta())); + + MetaValueSelector metaValueSelector = this.queryOptions.option(MetaValueSelector.KEY) + .orElseGet(() -> this.plugin.getConfiguration().get(ConfigKeys.META_VALUE_SELECTOR)); + + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (Map.Entry>> e : meta.entrySet()) { + if (e.getValue().isEmpty()) { + continue; + } + + Result selected = metaValueSelector.selectValue(e.getKey(), e.getValue()); + if (selected == null) { + throw new NullPointerException(metaValueSelector + " returned null"); + } + + builder.put(e.getKey(), (StringResult) selected); + } + this.flattenedMeta = builder.build(); + this.meta = new LowerCaseMetaMap(meta); + + this.prefixes = ImmutableSortedMap.copyOfSorted(sourceMeta.getPrefixes()); + this.suffixes = ImmutableSortedMap.copyOfSorted(sourceMeta.getSuffixes()); + this.weight = sourceMeta.getWeight(); + this.primaryGroup = sourceMeta.getPrimaryGroup(); + this.prefixDefinition = sourceMeta.getPrefixDefinition(); + this.suffixDefinition = sourceMeta.getSuffixDefinition(); + this.prefix = sourceMeta.getPrefix(); + this.suffix = sourceMeta.getSuffix(); + } + + public @NonNull StringResult getMetaValue(String key, CheckOrigin origin) { + Objects.requireNonNull(key, "key"); + return this.flattenedMeta.getOrDefault(key.toLowerCase(Locale.ROOT), StringResult.nullResult()); + } + + public @NonNull StringResult getPrefix(CheckOrigin origin) { + return this.prefix; + } + + public @NonNull StringResult getSuffix(CheckOrigin origin) { + return this.suffix; + } + + public @NonNull IntegerResult getWeight(CheckOrigin origin) { + return this.weight; + } + + public @NonNull Map>> getMetaResults(CheckOrigin origin) { + return this.meta; + } + + public @Nullable String getPrimaryGroup(CheckOrigin origin) { + return this.primaryGroup; + } + + public final Map> getMeta(CheckOrigin origin) { + return Maps.transformValues(getMetaResults(origin), list -> Lists.transform(list, StringResult::result)); + } + + public @Nullable String getMetaOrChatMetaValue(String key, CheckOrigin origin) { + if (key.equals(Prefix.NODE_KEY)) { + return getPrefix(origin).result(); + } else if (key.equals(Suffix.NODE_KEY)) { + return getSuffix(origin).result(); + } else { + return getMetaValue(key, origin).result(); + } + } + + @Override + public final @NonNull Result queryMetaValue(@NonNull String key) { + return getMetaValue(key, CheckOrigin.LUCKPERMS_API); + } + + @Override + public final @NonNull Result queryPrefix() { + return getPrefix(CheckOrigin.LUCKPERMS_API); + } + + @Override + public final @NonNull Result querySuffix() { + return getSuffix(CheckOrigin.LUCKPERMS_API); + } + + @Override + public @NonNull Result queryWeight() { + return getWeight(CheckOrigin.LUCKPERMS_API); + } + + @Override + public final @NonNull Map> getMeta() { + return getMeta(CheckOrigin.LUCKPERMS_API); } @Override - public String getMetaValue(String key, MetaCheckEvent.Origin origin) { - String value = super.getMetaValue(key, origin); - this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), key, String.valueOf(value)); - return value; + public final @Nullable String getPrimaryGroup() { + return getPrimaryGroup(CheckOrigin.LUCKPERMS_API); } @Override - public String getPrefix(MetaCheckEvent.Origin origin) { - String value = super.getPrefix(origin); - this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), Prefix.NODE_KEY, String.valueOf(value)); - return value; + public @NonNull SortedMap getPrefixes() { + return Maps.transformValues(this.prefixes, StringResult::result); } @Override - public String getSuffix(MetaCheckEvent.Origin origin) { - String value = super.getSuffix(origin); - this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), Suffix.NODE_KEY, String.valueOf(value)); - return value; + public @NonNull SortedMap getSuffixes() { + return Maps.transformValues(this.suffixes, StringResult::result); } @Override - public Map> getMeta(MetaCheckEvent.Origin origin) { - return new MonitoredMetaMap(super.getMeta(origin), origin); + public @NonNull MetaStackDefinition getPrefixStackDefinition() { + return this.prefixDefinition; } @Override - public int getWeight(MetaCheckEvent.Origin origin) { - int value = super.getWeight(origin); - this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), "weight", String.valueOf(value)); - return value; + public @NonNull MetaStackDefinition getSuffixStackDefinition() { + return this.suffixDefinition; } @Override - public @Nullable String getPrimaryGroup(MetaCheckEvent.Origin origin) { - String value = super.getPrimaryGroup(origin); - this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), "primarygroup", String.valueOf(value)); - return value; + public @NonNull QueryOptions getQueryOptions() { + return this.queryOptions; } - private final class MonitoredMetaMap extends ForwardingMap> { - private final Map> delegate; - private final MetaCheckEvent.Origin origin; + private static final class LowerCaseMetaMap extends ForwardingMap>> { + private final Map>> delegate; - private MonitoredMetaMap(Map> delegate, MetaCheckEvent.Origin origin) { + private LowerCaseMetaMap(Map>> delegate) { this.delegate = delegate; - this.origin = origin; } @Override - protected Map> delegate() { + protected Map>> delegate() { return this.delegate; } @Override - public List get(Object k) { + public List> get(Object k) { if (k == null) { return null; } String key = (String) k; - List values = super.get(key); - MetaCache.this.plugin.getVerboseHandler().offerMetaCheckEvent(this.origin, MetaCache.this.metadata.getVerboseCheckInfo(), MetaCache.this.metadata.getQueryOptions(), key, String.valueOf(values)); - return values; + return super.get(key.toLowerCase(Locale.ROOT)); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaStackAccumulator.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaStackAccumulator.java index 89caedbd2..031ad7c34 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaStackAccumulator.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MetaStackAccumulator.java @@ -25,21 +25,23 @@ package me.lucko.luckperms.common.cacheddata.type; +import me.lucko.luckperms.common.cacheddata.result.StringResult; import net.luckperms.api.metastacking.MetaStackDefinition; import net.luckperms.api.metastacking.MetaStackElement; import net.luckperms.api.node.ChatMetaType; import net.luckperms.api.node.types.ChatMetaNode; - import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; -public class MetaStackAccumulator { +public class MetaStackAccumulator> { private final MetaStackDefinition definition; - private final List entries; + private final List> entries; public MetaStackAccumulator(MetaStackDefinition definition, ChatMetaType targetType) { this.definition = definition; @@ -47,20 +49,27 @@ public MetaStackAccumulator(MetaStackDefinition definition, ChatMetaType targetT List elements = definition.getElements(); this.entries = new ArrayList<>(elements.size()); for (MetaStackElement element : elements) { - this.entries.add(new Entry(element, targetType)); + this.entries.add(new Entry<>(element, targetType)); } } - public void offer(ChatMetaNode node) { - for (Entry entry : this.entries) { + public void offer(N node) { + for (Entry entry : this.entries) { entry.offer(node); } } + public List getElements() { + return this.entries.stream() + .map(Entry::getNode) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + public String toFormattedString() { List elements = new LinkedList<>(); - for (Entry entry : this.entries) { - ChatMetaNode node = entry.getNode(); + for (Entry entry : this.entries) { + N node = entry.getNode(); if (node != null) { elements.add(node.getMetaValue()); } @@ -94,22 +103,51 @@ public String toFormattedString() { return sb.toString(); } - private static final class Entry { + public StringResult toResult() { + String formatted = toFormattedString(); + if (formatted == null) { + return StringResult.nullResult(); + } + + List elements = getElements(); + + switch (elements.size()) { + case 0: + throw new AssertionError(); + case 1: + return StringResult.of(formatted, elements.get(0)); + default: { + Iterator it = elements.iterator(); + StringResult result = StringResult.of(formatted, it.next()); + + StringResult root = result; + while (it.hasNext()) { + StringResult nested = StringResult.of(it.next()); + root.setOverriddenResult(nested); + root = nested; + } + + return result; + } + } + } + + private static final class Entry> { private final MetaStackElement element; private final ChatMetaType type; - private @Nullable ChatMetaNode current = null; + private @Nullable N current = null; Entry(MetaStackElement element, ChatMetaType type) { this.element = element; this.type = type; } - public ChatMetaNode getNode() { + public N getNode() { return this.current; } - public boolean offer(ChatMetaNode node) { + public boolean offer(N node) { if (this.element.shouldAccumulate(this.type, node, this.current)) { this.current = node; return true; diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MonitoredMetaCache.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MonitoredMetaCache.java new file mode 100644 index 000000000..0b4498233 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/MonitoredMetaCache.java @@ -0,0 +1,149 @@ +/* + * 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.cacheddata.type; + +import com.google.common.collect.ForwardingMap; +import me.lucko.luckperms.common.cacheddata.CacheMetadata; +import me.lucko.luckperms.common.cacheddata.result.IntegerResult; +import me.lucko.luckperms.common.cacheddata.result.StringResult; +import me.lucko.luckperms.common.node.types.Prefix; +import me.lucko.luckperms.common.node.types.Suffix; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import net.luckperms.api.cacheddata.CachedMetaData; +import net.luckperms.api.node.types.MetaNode; +import net.luckperms.api.node.types.PrefixNode; +import net.luckperms.api.node.types.SuffixNode; +import net.luckperms.api.node.types.WeightNode; +import net.luckperms.api.query.QueryOptions; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Holds cached meta for a given context + */ +public class MonitoredMetaCache extends MetaCache implements CachedMetaData { + + /** The plugin instance */ + private final LuckPermsPlugin plugin; + + /** The metadata for this cache */ + private final CacheMetadata metadata; + + public MonitoredMetaCache(LuckPermsPlugin plugin, QueryOptions queryOptions, CacheMetadata metadata, MetaAccumulator sourceMeta) { + super(plugin, queryOptions, sourceMeta); + this.plugin = plugin; + this.metadata = metadata; + } + + @Override + public @NonNull StringResult getMetaValue(String key, CheckOrigin origin) { + StringResult value = super.getMetaValue(key, origin); + this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), key, value); + return value; + } + + @Override + public @NonNull StringResult getPrefix(CheckOrigin origin) { + StringResult value = super.getPrefix(origin); + this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), Prefix.NODE_KEY, value); + return value; + } + + @Override + public @NonNull StringResult getSuffix(CheckOrigin origin) { + StringResult value = super.getSuffix(origin); + this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), Suffix.NODE_KEY, value); + return value; + } + + @Override + public @NonNull Map>> getMetaResults(CheckOrigin origin) { + return new MonitoredMetaMap(super.getMetaResults(origin), origin); + } + + @Override + public @NonNull IntegerResult getWeight(CheckOrigin origin) { + IntegerResult value = super.getWeight(origin); + this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), "weight", value.asStringResult()); + return value; + } + + @Override + public @Nullable String getPrimaryGroup(CheckOrigin origin) { + String value = super.getPrimaryGroup(origin); + this.plugin.getVerboseHandler().offerMetaCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), "primarygroup", StringResult.of(value)); + return value; + } + + private final class MonitoredMetaMap extends ForwardingMap>> { + private final Map>> delegate; + private final CheckOrigin origin; + + private MonitoredMetaMap(Map>> delegate, CheckOrigin origin) { + this.delegate = delegate; + this.origin = origin; + } + + @Override + protected Map>> delegate() { + return this.delegate; + } + + @Override + public List> get(Object k) { + if (k == null) { + return null; + } + + String key = (String) k; + List> values = super.get(key); + + if (values == null || values.isEmpty()) { + MonitoredMetaCache.this.plugin.getVerboseHandler().offerMetaCheckEvent(this.origin, MonitoredMetaCache.this.metadata.getVerboseCheckInfo(), MonitoredMetaCache.this.metadata.getQueryOptions(), key, StringResult.nullResult()); + } else { + Iterator> it = values.iterator(); + StringResult result = it.next().copy(); + + StringResult root = result; + while (it.hasNext()) { + StringResult nested = it.next().copy(); + root.setOverriddenResult(nested); + root = nested; + } + + MonitoredMetaCache.this.plugin.getVerboseHandler().offerMetaCheckEvent(this.origin, MonitoredMetaCache.this.metadata.getVerboseCheckInfo(), MonitoredMetaCache.this.metadata.getQueryOptions(), key, result); + } + + return values; + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/PermissionCache.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/PermissionCache.java index 652a46c9a..ac0a0fdf3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/PermissionCache.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/PermissionCache.java @@ -25,17 +25,18 @@ package me.lucko.luckperms.common.cacheddata.type; +import com.google.common.collect.Maps; import me.lucko.luckperms.common.cacheddata.CacheMetadata; import me.lucko.luckperms.common.cacheddata.UsageTracked; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.calculator.PermissionCalculator; -import me.lucko.luckperms.common.calculator.result.TristateResult; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.cacheddata.CachedPermissionData; +import net.luckperms.api.cacheddata.Result; +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Collections; @@ -55,12 +56,12 @@ public class PermissionCache extends UsageTracked implements CachedPermissionDat /** * The raw set of permission strings. */ - private final Map permissions; + private final Map permissions; /** - * An immutable copy of {@link #permissions} + * A string->boolean view of {@link #permissions} */ - private final Map permissionsUnmodifiable; + private final Map permissionsView; /** * The calculator instance responsible for resolving the raw permission strings in the permission map. @@ -69,13 +70,11 @@ public class PermissionCache extends UsageTracked implements CachedPermissionDat */ private final PermissionCalculator calculator; - public PermissionCache(QueryOptions queryOptions, CacheMetadata metadata, CalculatorFactory calculatorFactory, ConcurrentHashMap sourcePermissions) { + public PermissionCache(QueryOptions queryOptions, CacheMetadata metadata, CalculatorFactory calculatorFactory, ConcurrentHashMap sourcePermissions) { this.queryOptions = queryOptions; this.permissions = sourcePermissions; - this.permissionsUnmodifiable = Collections.unmodifiableMap(this.permissions); - - this.calculator = calculatorFactory.build(queryOptions, metadata); - this.calculator.setSourcePermissions(this.permissions); + this.permissionsView = Collections.unmodifiableMap(Maps.transformValues(this.permissions, Node::getValue)); + this.calculator = calculatorFactory.build(queryOptions, this.permissions, metadata); } @Override @@ -89,19 +88,24 @@ public PermissionCalculator getCalculator() { @Override public @NonNull Map getPermissionMap() { - return this.permissionsUnmodifiable; + return this.permissionsView; } - public TristateResult checkPermission(String permission, PermissionCheckEvent.Origin origin) { + public TristateResult checkPermission(String permission, CheckOrigin origin) { if (permission == null) { throw new NullPointerException("permission"); } return this.calculator.checkPermission(permission, origin); } + @Override + public @NonNull Result queryPermission(@NonNull String permission) { + return checkPermission(permission, CheckOrigin.LUCKPERMS_API); + } + @Override public @NonNull Tristate checkPermission(@NonNull String permission) { - return checkPermission(permission, PermissionCheckEvent.Origin.LUCKPERMS_API).result(); + return checkPermission(permission, CheckOrigin.LUCKPERMS_API).result(); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/SimpleMetaCache.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/SimpleMetaCache.java deleted file mode 100644 index 66909a798..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/SimpleMetaCache.java +++ /dev/null @@ -1,212 +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.common.cacheddata.type; - -import com.google.common.collect.ForwardingMap; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSortedMap; -import com.google.common.collect.Multimaps; - -import me.lucko.luckperms.common.cacheddata.UsageTracked; -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; - -import net.luckperms.api.cacheddata.CachedMetaData; -import net.luckperms.api.metastacking.MetaStackDefinition; -import net.luckperms.api.query.QueryOptions; -import net.luckperms.api.query.meta.MetaValueSelector; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.SortedMap; - -/** - * Holds cached meta for a given context - */ -public class SimpleMetaCache extends UsageTracked implements CachedMetaData { - - private final LuckPermsPlugin plugin; - - /** The query options this container is holding data for */ - private final QueryOptions queryOptions; - - /* The data */ - protected final Map> meta; - protected final Map flattenedMeta; - protected final SortedMap prefixes; - protected final SortedMap suffixes; - protected final int weight; - protected final String primaryGroup; - private final MetaStackDefinition prefixDefinition; - private final MetaStackDefinition suffixDefinition; - private final String prefix; - private final String suffix; - - public SimpleMetaCache(LuckPermsPlugin plugin, QueryOptions queryOptions, MetaAccumulator sourceMeta) { - this.plugin = plugin; - this.queryOptions = queryOptions; - - Map> meta = Multimaps.asMap(ImmutableListMultimap.copyOf(sourceMeta.getMeta())); - - MetaValueSelector metaValueSelector = this.queryOptions.option(MetaValueSelector.KEY) - .orElseGet(() -> this.plugin.getConfiguration().get(ConfigKeys.META_VALUE_SELECTOR)); - - ImmutableMap.Builder builder = ImmutableMap.builder(); - for (Map.Entry> e : meta.entrySet()) { - if (e.getValue().isEmpty()) { - continue; - } - - String selected = metaValueSelector.selectValue(e.getKey(), e.getValue()); - if (selected == null) { - throw new NullPointerException(metaValueSelector + " returned null"); - } - - builder.put(e.getKey(), selected); - } - this.flattenedMeta = builder.build(); - this.meta = new LowerCaseMetaMap(meta); - - this.prefixes = ImmutableSortedMap.copyOfSorted(sourceMeta.getPrefixes()); - this.suffixes = ImmutableSortedMap.copyOfSorted(sourceMeta.getSuffixes()); - this.weight = sourceMeta.getWeight(); - this.primaryGroup = sourceMeta.getPrimaryGroup(); - this.prefixDefinition = sourceMeta.getPrefixDefinition(); - this.suffixDefinition = sourceMeta.getSuffixDefinition(); - this.prefix = sourceMeta.getPrefix(); - this.suffix = sourceMeta.getSuffix(); - } - - public String getMetaValue(String key, MetaCheckEvent.Origin origin) { - Objects.requireNonNull(key, "key"); - return this.flattenedMeta.get(key.toLowerCase()); - } - - @Override - public final String getMetaValue(@NonNull String key) { - return getMetaValue(key, MetaCheckEvent.Origin.LUCKPERMS_API); - } - - public String getPrefix(MetaCheckEvent.Origin origin) { - return this.prefix; - } - - @Override - public final String getPrefix() { - return getPrefix(MetaCheckEvent.Origin.LUCKPERMS_API); - } - - public String getSuffix(MetaCheckEvent.Origin origin) { - return this.suffix; - } - - @Override - public final String getSuffix() { - return getSuffix(MetaCheckEvent.Origin.LUCKPERMS_API); - } - - public Map> getMeta(MetaCheckEvent.Origin origin) { - return this.meta; - } - - @Override - public final @NonNull Map> getMeta() { - return getMeta(MetaCheckEvent.Origin.LUCKPERMS_API); - } - - @Override - public @NonNull SortedMap getPrefixes() { - return this.prefixes; - } - - @Override - public @NonNull SortedMap getSuffixes() { - return this.suffixes; - } - - public int getWeight(MetaCheckEvent.Origin origin) { - return this.weight; - } - - //@Override - not actually exposed in the API atm - public final int getWeight() { - return getWeight(MetaCheckEvent.Origin.LUCKPERMS_API); - } - - public @Nullable String getPrimaryGroup(MetaCheckEvent.Origin origin) { - return this.primaryGroup; - } - - @Override - public final @Nullable String getPrimaryGroup() { - return getPrimaryGroup(MetaCheckEvent.Origin.LUCKPERMS_API); - } - - @Override - public @NonNull MetaStackDefinition getPrefixStackDefinition() { - return this.prefixDefinition; - } - - @Override - public @NonNull MetaStackDefinition getSuffixStackDefinition() { - return this.suffixDefinition; - } - - @Override - public @NonNull QueryOptions getQueryOptions() { - return this.queryOptions; - } - - private static final class LowerCaseMetaMap extends ForwardingMap> { - private final Map> delegate; - - private LowerCaseMetaMap(Map> delegate) { - this.delegate = delegate; - } - - @Override - protected Map> delegate() { - return this.delegate; - } - - @Override - public List get(Object k) { - if (k == null) { - return null; - } - - String key = (String) k; - return super.get(key.toLowerCase()); - } - } - -} diff --git a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/SimpleMetaValueSelector.java b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/SimpleMetaValueSelector.java index 2aa48cdfd..26004cdc9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/SimpleMetaValueSelector.java +++ b/common/src/main/java/me/lucko/luckperms/common/cacheddata/type/SimpleMetaValueSelector.java @@ -25,11 +25,13 @@ package me.lucko.luckperms.common.cacheddata.type; +import net.luckperms.api.cacheddata.Result; +import net.luckperms.api.node.types.MetaNode; import net.luckperms.api.query.meta.MetaValueSelector; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; +import java.util.Locale; import java.util.Map; public class SimpleMetaValueSelector implements MetaValueSelector { @@ -42,7 +44,7 @@ public SimpleMetaValueSelector(Map strategies, Strategy defaul } @Override - public @NonNull String selectValue(@NonNull String key, @NonNull List values) { + public @NonNull Result selectValue(@NonNull String key, @NonNull List> values) { switch (values.size()) { case 0: throw new IllegalArgumentException("values is empty"); @@ -56,7 +58,7 @@ public SimpleMetaValueSelector(Map strategies, Strategy defaul public enum Strategy { INHERITANCE { @Override - public String select(List values) { + public Result select(List> values) { return values.get(0); } }, @@ -64,7 +66,7 @@ public String select(List values) { private final DoubleSelectionPredicate selection = (value, current) -> value > current; @Override - public String select(List values) { + public Result select(List> values) { return selectNumber(values, this.selection); } }, @@ -72,16 +74,16 @@ public String select(List values) { private final DoubleSelectionPredicate selection = (value, current) -> value < current; @Override - public String select(List values) { + public Result select(List> values) { return selectNumber(values, this.selection); } }; - public abstract String select(List values); + public abstract Result select(List> values); public static Strategy parse(String s) { try { - return Strategy.valueOf(s.replace('-', '_').toUpperCase()); + return Strategy.valueOf(s.replace('-', '_').toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException e) { return null; } @@ -93,15 +95,15 @@ private interface DoubleSelectionPredicate { boolean shouldSelect(double value, double current); } - private static String selectNumber(List values, DoubleSelectionPredicate selection) { + private static Result selectNumber(List> values, DoubleSelectionPredicate selection) { double current = 0; - String selected = null; + Result selected = null; - for (String value : values) { + for (Result result : values) { try { - double parse = Double.parseDouble(value); + double parse = Double.parseDouble(result.result()); if (selected == null || selection.shouldSelect(parse, current)) { - selected = value; + selected = result; current = parse; } } catch (NumberFormatException e) { diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/CalculatorFactory.java b/common/src/main/java/me/lucko/luckperms/common/calculator/CalculatorFactory.java index 1af131d6d..7d2e8ff83 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculator/CalculatorFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/CalculatorFactory.java @@ -26,21 +26,24 @@ package me.lucko.luckperms.common.calculator; import me.lucko.luckperms.common.cacheddata.CacheMetadata; - +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; +import java.util.Map; + /** - * Creates a calculator instance given a set of contexts + * Creates {@link PermissionCalculator} instances */ public interface CalculatorFactory { /** - * Builds a PermissionCalculator for the user in the given context + * Builds a PermissionCalculator with the given source permissions and query options. * - * @param queryOptions the query options to build the calculator for - * @param metadata the calculator metadata + * @param queryOptions the query options + * @param sourceMap the source permissions map + * @param metadata the calculator metadata * @return a permission calculator instance */ - PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata); + PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata); } diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculator.java b/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculator.java index d878b4095..0595ed731 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculator.java +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculator.java @@ -25,97 +25,34 @@ package me.lucko.luckperms.common.calculator; -import me.lucko.luckperms.common.cache.LoadingMap; -import me.lucko.luckperms.common.cacheddata.CacheMetadata; -import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; -import org.checkerframework.checker.nullness.qual.NonNull; +public interface PermissionCalculator { -import java.util.Collection; -import java.util.Map; -import java.util.function.Function; - -/** - * Calculates and caches permissions - */ -public class PermissionCalculator implements Function { - - /** The plugin instance */ - private final LuckPermsPlugin plugin; - - /** Info about the nature of this calculator. */ - private final CacheMetadata metadata; - - /** The processors which back this calculator */ - private final PermissionProcessor[] processors; + /** + * A calculator which always returns undefined. + */ + PermissionCalculator EMPTY = new PermissionCalculator() { + @Override + public TristateResult checkPermission(String permission, CheckOrigin origin) { + return TristateResult.UNDEFINED; + } - /** Loading cache for permission checks */ - private final LoadingMap lookupCache = LoadingMap.of(this); + @Override + public void invalidateCache() { - public PermissionCalculator(LuckPermsPlugin plugin, CacheMetadata metadata, Collection processors) { - this.plugin = plugin; - this.metadata = metadata; - this.processors = processors.toArray(new PermissionProcessor[0]); - } + } + }; /** * Performs a permission check against this calculator. * - *

    The result is calculated using the calculators backing 'processors'.

    - * * @param permission the permission to check * @param origin marks where this check originated from * @return the result */ - public TristateResult checkPermission(String permission, PermissionCheckEvent.Origin origin) { - // get the result - TristateResult result = this.lookupCache.get(permission); - - // log this permission lookup to the verbose handler - this.plugin.getVerboseHandler().offerPermissionCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), permission, result); - - // return the result - return result; - } - - @Override - public TristateResult apply(@NonNull String permission) { - // convert the permission to lowercase, as all values in the backing map are also lowercase. - // this allows fast case insensitive lookups - permission = permission.toLowerCase(); - - // offer the permission to the permission vault - // we only need to do this once per permission, so it doesn't matter - // that this call is behind the cache. - this.plugin.getPermissionRegistry().offer(permission); - - TristateResult result = TristateResult.UNDEFINED; - for (PermissionProcessor processor : this.processors) { - result = processor.hasPermission(result, permission); - } - return result; - } - - /** - * Defines the source permissions map which should be used when calculating - * a result. - * - * @param sourceMap the source map - */ - public synchronized void setSourcePermissions(Map sourceMap) { - for (PermissionProcessor processor : this.processors) { - processor.setSource(sourceMap); - processor.refresh(); - } - } + TristateResult checkPermission(String permission, CheckOrigin origin); - public void invalidateCache() { - for (PermissionProcessor processor : this.processors) { - processor.invalidate(); - } - this.lookupCache.clear(); - } + void invalidateCache(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculatorBase.java b/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculatorBase.java new file mode 100644 index 000000000..6abad7bc4 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculatorBase.java @@ -0,0 +1,82 @@ +/* + * 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.calculator; + +import me.lucko.luckperms.common.cache.LoadingMap; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Collection; +import java.util.Locale; + +/** + * Calculates and caches permissions + */ +public class PermissionCalculatorBase implements PermissionCalculator { + + /** The processors which back this calculator */ + private final PermissionProcessor[] processors; + + /** Loading cache for permission checks */ + private final LoadingMap lookupCache = LoadingMap.of(this::resolve); + + public PermissionCalculatorBase(Collection processors) { + this.processors = processors.toArray(new PermissionProcessor[0]); + } + + @Override + public TristateResult checkPermission(String permission, CheckOrigin origin) { + return this.lookupCache.get(permission); + } + + private TristateResult resolve(@NonNull String permission) { + // convert the permission to lowercase, as all values in the backing map are also lowercase. + // this allows fast case insensitive lookups + permission = permission.toLowerCase(Locale.ROOT); + + observePermission(permission); + + TristateResult result = TristateResult.UNDEFINED; + for (PermissionProcessor processor : this.processors) { + result = processor.hasPermission(result, permission); + } + return result; + } + + protected void observePermission(String permission) { + + } + + @Override + public void invalidateCache() { + for (PermissionProcessor processor : this.processors) { + processor.invalidate(); + } + this.lookupCache.clear(); + } +} \ No newline at end of file diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculatorMonitored.java b/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculatorMonitored.java new file mode 100644 index 000000000..9f17a997a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/PermissionCalculatorMonitored.java @@ -0,0 +1,67 @@ +/* + * 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.calculator; + +import me.lucko.luckperms.common.cacheddata.CacheMetadata; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; + +import java.util.Collection; + +/** + * Calculates and caches permissions + */ +public class PermissionCalculatorMonitored extends PermissionCalculatorBase { + + /** The plugin instance */ + private final LuckPermsPlugin plugin; + + /** Info about the nature of this calculator. */ + private final CacheMetadata metadata; + + public PermissionCalculatorMonitored(LuckPermsPlugin plugin, CacheMetadata metadata, Collection processors) { + super(processors); + this.plugin = plugin; + this.metadata = metadata; + } + + @Override + public TristateResult checkPermission(String permission, CheckOrigin origin) { + TristateResult result = super.checkPermission(permission, origin); + this.plugin.getVerboseHandler().offerPermissionCheckEvent(origin, this.metadata.getVerboseCheckInfo(), this.metadata.getQueryOptions(), permission, result); + return result; + } + + @Override + protected void observePermission(String permission) { + // offer the permission to the permission vault + // we only need to do this once per permission, so it doesn't matter + // that this call is behind the cache. + this.plugin.getPermissionRegistry().offer(permission); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/AbstractOverrideWildcardProcessor.java b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/AbstractOverrideWildcardProcessor.java new file mode 100644 index 000000000..89012544e --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/AbstractOverrideWildcardProcessor.java @@ -0,0 +1,57 @@ +/* + * 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.calculator.processor; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import net.luckperms.api.util.Tristate; + +public abstract class AbstractOverrideWildcardProcessor extends AbstractPermissionProcessor implements PermissionProcessor { + private final boolean overrideWildcards; + + public AbstractOverrideWildcardProcessor(boolean overrideWildcards) { + this.overrideWildcards = overrideWildcards; + } + + private boolean canOverrideWildcard(TristateResult prev) { + return this.overrideWildcards && + (prev.processorClass() == WildcardProcessor.class || prev.processorClass() == SpongeWildcardProcessor.class) && + prev.result() == Tristate.TRUE; + } + + @Override + protected TristateResult hasPermissionOverride(TristateResult prev, String permission) { + if (canOverrideWildcard(prev)) { + TristateResult override = hasPermission(permission); + if (override.result() == Tristate.FALSE) { + override.setOverriddenResult(prev); + return override; + } + } + + return prev; + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/AbstractPermissionProcessor.java b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/AbstractPermissionProcessor.java index 198a7b69d..05fb3f705 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/AbstractPermissionProcessor.java +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/AbstractPermissionProcessor.java @@ -25,26 +25,28 @@ package me.lucko.luckperms.common.calculator.processor; -import me.lucko.luckperms.common.calculator.result.TristateResult; - -import java.util.Collections; -import java.util.Map; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +/** + * Abstract implementation of {@link PermissionProcessor} that splits behaviour for normal + * permission checks and override checks into two separate methods. + */ public abstract class AbstractPermissionProcessor implements PermissionProcessor { - protected Map sourceMap = Collections.emptyMap(); - - @Override - public void setSource(Map sourceMap) { - this.sourceMap = sourceMap; - } @Override - public TristateResult hasPermission(TristateResult prev, String permission) { + public final TristateResult hasPermission(TristateResult prev, String permission) { if (prev != TristateResult.UNDEFINED) { - return prev; + return hasPermissionOverride(prev, permission); } return hasPermission(permission); } - public abstract TristateResult hasPermission(String permission); + // Performs a regular permission check + protected abstract TristateResult hasPermission(String permission); + + // Performs an override permission check + protected TristateResult hasPermissionOverride(TristateResult prev, String permission) { + return prev; + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/DirectProcessor.java b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/DirectProcessor.java index 2a19212d5..679743547 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/DirectProcessor.java +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/DirectProcessor.java @@ -25,16 +25,23 @@ package me.lucko.luckperms.common.calculator.processor; -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import net.luckperms.api.node.Node; -import net.luckperms.api.util.Tristate; +import java.util.Map; public class DirectProcessor extends AbstractPermissionProcessor implements PermissionProcessor { private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(DirectProcessor.class); + private final Map sourceMap; + + public DirectProcessor(Map sourceMap) { + this.sourceMap = sourceMap; + } + @Override public TristateResult hasPermission(String permission) { - return RESULT_FACTORY.result(Tristate.of(this.sourceMap.get(permission))); + return RESULT_FACTORY.result(this.sourceMap.get(permission)); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/PermissionProcessor.java b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/PermissionProcessor.java index 0bca9d5e4..aea650f26 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/PermissionProcessor.java +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/PermissionProcessor.java @@ -25,10 +25,8 @@ package me.lucko.luckperms.common.calculator.processor; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.calculator.PermissionCalculator; -import me.lucko.luckperms.common.calculator.result.TristateResult; - -import java.util.Map; /** * A processor within a {@link PermissionCalculator}. @@ -47,22 +45,6 @@ public interface PermissionProcessor { */ TristateResult hasPermission(TristateResult prev, String permission); - /** - * Sets the source permissions which should be used by this processor - * - * @param sourceMap the source map - */ - default void setSource(Map sourceMap) { - - } - - /** - * Called after a change has been made to the source map - */ - default void refresh() { - - } - /** * Called after the parent calculator has been invalidated */ diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/RegexProcessor.java b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/RegexProcessor.java index ae844f57c..b6a3bbbfe 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/RegexProcessor.java +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/RegexProcessor.java @@ -26,14 +26,10 @@ package me.lucko.luckperms.common.calculator.processor; import com.google.common.collect.ImmutableList; -import com.google.common.collect.Maps; - -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.node.types.RegexPermission; +import net.luckperms.api.node.Node; -import net.luckperms.api.util.Tristate; - -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -41,22 +37,25 @@ public class RegexProcessor extends AbstractPermissionProcessor implements PermissionProcessor { private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(RegexProcessor.class); - private List> regexPermissions = Collections.emptyList(); + private final List regexPermissions; + + public RegexProcessor(Map sourceMap) { + this.regexPermissions = process(sourceMap); + } @Override public TristateResult hasPermission(String permission) { - for (Map.Entry e : this.regexPermissions) { - if (e.getKey().matcher(permission).matches()) { - return e.getValue(); + for (RegexEntry e : this.regexPermissions) { + if (e.pattern().matcher(permission).matches()) { + return e.result(); } } return TristateResult.UNDEFINED; } - @Override - public void refresh() { - ImmutableList.Builder> builder = ImmutableList.builder(); - for (Map.Entry e : this.sourceMap.entrySet()) { + private static List process(Map sourceMap) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Map.Entry e : sourceMap.entrySet()) { RegexPermission.Builder regexPerm = RegexPermission.parse(e.getKey()); if (regexPerm == null) { continue; @@ -67,9 +66,27 @@ public void refresh() { continue; } - TristateResult value = RESULT_FACTORY.result(Tristate.of(e.getValue()), "pattern: " + pattern.pattern()); - builder.add(Maps.immutableEntry(pattern, value)); + TristateResult value = RESULT_FACTORY.result(e.getValue()); + builder.add(new RegexEntry(pattern, value)); + } + return builder.build(); + } + + private static final class RegexEntry { + private final Pattern pattern; + private final TristateResult result; + + RegexEntry(Pattern pattern, TristateResult result) { + this.pattern = pattern; + this.result = result; + } + + public Pattern pattern() { + return this.pattern; + } + + public TristateResult result() { + return this.result; } - this.regexPermissions = builder.build(); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/SpongeWildcardProcessor.java b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/SpongeWildcardProcessor.java index 382f29611..7ab750895 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/SpongeWildcardProcessor.java +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/SpongeWildcardProcessor.java @@ -25,14 +25,21 @@ package me.lucko.luckperms.common.calculator.processor; -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.node.AbstractNode; +import net.luckperms.api.node.Node; -import net.luckperms.api.util.Tristate; +import java.util.Map; public class SpongeWildcardProcessor extends AbstractPermissionProcessor implements PermissionProcessor { private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(SpongeWildcardProcessor.class); + private final Map sourceMap; + + public SpongeWildcardProcessor(Map sourceMap) { + this.sourceMap = sourceMap; + } + @Override public TristateResult hasPermission(String permission) { String node = permission; @@ -45,9 +52,9 @@ public TristateResult hasPermission(String permission) { node = node.substring(0, endIndex); if (!node.isEmpty()) { - Tristate t = Tristate.of(this.sourceMap.get(node)); - if (t != Tristate.UNDEFINED) { - return RESULT_FACTORY.result(t, "match: " + node); + Node n = this.sourceMap.get(node); + if (n != null) { + return RESULT_FACTORY.result(n); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/WildcardProcessor.java b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/WildcardProcessor.java index fbb0faab9..3c52d5a2d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/calculator/processor/WildcardProcessor.java +++ b/common/src/main/java/me/lucko/luckperms/common/calculator/processor/WildcardProcessor.java @@ -26,13 +26,11 @@ package me.lucko.luckperms.common.calculator.processor; import com.google.common.collect.ImmutableMap; - -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.node.AbstractNode; - +import net.luckperms.api.node.Node; import net.luckperms.api.util.Tristate; -import java.util.Collections; import java.util.Map; public class WildcardProcessor extends AbstractPermissionProcessor implements PermissionProcessor { @@ -42,17 +40,14 @@ public class WildcardProcessor extends AbstractPermissionProcessor implements Pe private static final String ROOT_WILDCARD = "*"; private static final String ROOT_WILDCARD_WITH_QUOTES = "'*'"; - public static boolean isRootWildcard(String permission) { - return ROOT_WILDCARD.equals(permission) || ROOT_WILDCARD_WITH_QUOTES.equals(permission); - } + private final Map wildcardPermissions; + private final TristateResult rootWildcardState; - public static boolean isWildcardPermission(String permission) { - return isRootWildcard(permission) || permission.endsWith(WILDCARD_SUFFIX) && permission.length() > 2; + public WildcardProcessor(Map sourceMap) { + this.wildcardPermissions = processWildcardPermissions(sourceMap); + this.rootWildcardState = rootWildcardState(sourceMap); } - private Map wildcardPermissions = Collections.emptyMap(); - private TristateResult rootWildcardState = TristateResult.UNDEFINED; - @Override public TristateResult hasPermission(String permission) { String node = permission; @@ -75,25 +70,34 @@ public TristateResult hasPermission(String permission) { return this.rootWildcardState; } - @Override - public void refresh() { + public static boolean isRootWildcard(String permission) { + return ROOT_WILDCARD.equals(permission) || ROOT_WILDCARD_WITH_QUOTES.equals(permission); + } + + public static boolean isWildcardPermission(String permission) { + return isRootWildcard(permission) || permission.endsWith(WILDCARD_SUFFIX) && permission.length() > 2; + } + + private static Map processWildcardPermissions(Map sourceMap) { ImmutableMap.Builder builder = ImmutableMap.builder(); - for (Map.Entry e : this.sourceMap.entrySet()) { + for (Map.Entry e : sourceMap.entrySet()) { String key = e.getKey(); if (!key.endsWith(WILDCARD_SUFFIX) || key.length() <= 2) { continue; } key = key.substring(0, key.length() - 2); - TristateResult value = RESULT_FACTORY.result(Tristate.of(e.getValue()), "match: " + key); + TristateResult value = RESULT_FACTORY.result(e.getValue()); builder.put(key, value); } - this.wildcardPermissions = builder.build(); + return builder.build(); + } - Tristate state = Tristate.of(this.sourceMap.get(ROOT_WILDCARD)); - if (state == Tristate.UNDEFINED) { - state = Tristate.of(this.sourceMap.get(ROOT_WILDCARD_WITH_QUOTES)); + private static TristateResult rootWildcardState(Map sourceMap) { + Node rootWildcard = sourceMap.get(ROOT_WILDCARD); + if (rootWildcard == null) { + rootWildcard = sourceMap.get(ROOT_WILDCARD_WITH_QUOTES); } - this.rootWildcardState = RESULT_FACTORY.result(state, "root"); + return rootWildcard == null ? TristateResult.UNDEFINED : RESULT_FACTORY.result(rootWildcard); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java b/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java index dbb9da189..8f3bfbdb2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/CommandManager.java @@ -25,9 +25,9 @@ package me.lucko.luckperms.common.command; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ThreadFactoryBuilder; - import me.lucko.luckperms.common.command.abstraction.Command; import me.lucko.luckperms.common.command.abstraction.CommandException; import me.lucko.luckperms.common.command.tabcomplete.CompletionSupplier; @@ -51,12 +51,14 @@ import me.lucko.luckperms.common.commands.misc.SyncCommand; import me.lucko.luckperms.common.commands.misc.TranslationsCommand; import me.lucko.luckperms.common.commands.misc.TreeCommand; +import me.lucko.luckperms.common.commands.misc.TrustEditorCommand; import me.lucko.luckperms.common.commands.misc.VerboseCommand; import me.lucko.luckperms.common.commands.track.CreateTrack; import me.lucko.luckperms.common.commands.track.DeleteTrack; import me.lucko.luckperms.common.commands.track.ListTracks; import me.lucko.luckperms.common.commands.track.TrackParentCommand; import me.lucko.luckperms.common.commands.user.UserParentCommand; +import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; @@ -64,8 +66,8 @@ import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.ExpiringSet; import me.lucko.luckperms.common.util.ImmutableCollectors; - import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; @@ -75,7 +77,10 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -97,6 +102,7 @@ public class CommandManager { .build() ); private final AtomicBoolean executingCommand = new AtomicBoolean(false); + private final Set playerRateLimit = ExpiringSet.newExpiringSet(500, TimeUnit.MILLISECONDS); private final TabCompletions tabCompletions; private final Map> mainCommands; @@ -122,6 +128,7 @@ public CommandManager(LuckPermsPlugin plugin) { .add(new BulkUpdateCommand()) .add(new TranslationsCommand()) .add(new ApplyEditsCommand()) + .add(new TrustEditorCommand()) .add(new CreateGroup()) .add(new DeleteGroup()) .add(new ListGroups()) @@ -130,7 +137,7 @@ public CommandManager(LuckPermsPlugin plugin) { .add(new ListTracks()) .build() .stream() - .collect(ImmutableCollectors.toMap(c -> c.getName().toLowerCase(), Function.identity())); + .collect(ImmutableCollectors.toMap(c -> c.getName().toLowerCase(Locale.ROOT), Function.identity())); } public LuckPermsPlugin getPlugin() { @@ -141,7 +148,27 @@ public TabCompletions getTabCompletions() { return this.tabCompletions; } + @VisibleForTesting + public Map> getMainCommands() { + return this.mainCommands; + } + public CompletableFuture executeCommand(Sender sender, String label, List args) { + UUID uniqueId = sender.getUniqueId(); + if (this.plugin.getConfiguration().get(ConfigKeys.COMMANDS_RATE_LIMIT) && !sender.isConsole() && !Sender.CONSOLE_UUID.equals(uniqueId) && !this.playerRateLimit.add(uniqueId)) { + this.plugin.getLogger().warn("Player '" + uniqueId + "' is spamming LuckPerms commands. Ignoring further inputs."); + return CompletableFuture.completedFuture(null); + } + + boolean commandsDisabled = sender.isConsole() + ? this.plugin.getConfiguration().get(ConfigKeys.DISABLE_LUCKPERMS_COMMANDS_CONSOLE) + : this.plugin.getConfiguration().get(ConfigKeys.DISABLE_LUCKPERMS_COMMANDS_PLAYERS); + + if (commandsDisabled) { + Message.COMMANDS_DISABLED.send(sender); + return CompletableFuture.completedFuture(null); + } + SchedulerAdapter scheduler = this.plugin.getBootstrap().getScheduler(); List argsCopy = new ArrayList<>(args); @@ -235,7 +262,7 @@ private void execute(Sender sender, String label, List arguments) { } // Look for the main command. - Command main = this.mainCommands.get(arguments.get(0).toLowerCase()); + Command main = this.mainCommands.get(arguments.get(0).toLowerCase(Locale.ROOT)); // Main command not found if (main == null) { @@ -268,6 +295,14 @@ private void execute(Sender sender, String label, List arguments) { } public List tabCompleteCommand(Sender sender, List arguments) { + boolean commandsDisabled = sender.isConsole() + ? this.plugin.getConfiguration().get(ConfigKeys.DISABLE_LUCKPERMS_COMMANDS_CONSOLE) + : this.plugin.getConfiguration().get(ConfigKeys.DISABLE_LUCKPERMS_COMMANDS_PLAYERS); + + if (commandsDisabled) { + return Collections.emptyList(); + } + applyConvenienceAliases(arguments, false); final List> mains = this.mainCommands.values().stream() @@ -276,7 +311,7 @@ public List tabCompleteCommand(Sender sender, List arguments) { .collect(Collectors.toList()); return TabCompleter.create() - .at(0, CompletionSupplier.startsWith(() -> mains.stream().map(c -> c.getName().toLowerCase()))) + .at(0, CompletionSupplier.startsWith(() -> mains.stream().map(c -> c.getName().toLowerCase(Locale.ROOT)))) .from(1, partial -> mains.stream() .filter(m -> m.getName().equalsIgnoreCase(arguments.get(0))) .findFirst() @@ -333,7 +368,7 @@ private static void applyConvenienceAliases(List args, boolean rewriteLa // '/lp user Luck p set --> /lp user Luck permission set' etc // ^ ^^^^^^^^^^ if (args.size() >= 3 && (rewriteLastArgument || args.size() >= 4)) { - String arg0 = args.get(0).toLowerCase(); + String arg0 = args.get(0).toLowerCase(Locale.ROOT); if (arg0.equals("user") || arg0.equals("group")) { replaceArgs(args, 2, arg -> { switch (arg) { @@ -353,7 +388,7 @@ private static void applyConvenienceAliases(List args, boolean rewriteLa // '/lp user Luck permission i' --> '/lp user Luck permission info' etc // ^ ^^^^ if (args.size() >= 4 && (rewriteLastArgument || args.size() >= 5)) { - String arg2 = args.get(2).toLowerCase(); + String arg2 = args.get(2).toLowerCase(Locale.ROOT); if (arg2.equals("permission") || arg2.equals("parent") || arg2.equals("meta")) { replaceArgs(args, 3, arg -> arg.equals("i") ? "info" : null); } @@ -363,7 +398,7 @@ private static void applyConvenienceAliases(List args, boolean rewriteLa } private static void replaceArgs(List args, int i, Function rewrites) { - String arg = args.get(i).toLowerCase(); + String arg = args.get(i).toLowerCase(Locale.ROOT); String rewrite = rewrites.apply(arg); if (rewrite != null) { args.remove(i); diff --git a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ChildCommand.java b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ChildCommand.java index a3f6cc67e..ea894bfbc 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ChildCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ChildCommand.java @@ -30,12 +30,13 @@ import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.sender.Sender; - import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import java.util.List; +import java.util.Locale; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -58,7 +59,7 @@ public void sendUsage(Sender sender, String label) { TextComponent.Builder builder = Component.text() .append(Component.text('>', NamedTextColor.DARK_AQUA)) .append(Component.space()) - .append(Component.text(getName().toLowerCase(), NamedTextColor.GREEN)); + .append(Component.text(getName().toLowerCase(Locale.ROOT), NamedTextColor.GREEN)); if (getArgs().isPresent()) { List argUsages = getArgs().get().stream() @@ -66,7 +67,7 @@ public void sendUsage(Sender sender, String label) { .collect(Collectors.toList()); builder.append(Component.text(" - ", NamedTextColor.DARK_AQUA)) - .append(Component.join(Component.space(), argUsages)) + .append(Component.join(JoinConfiguration.separator(Component.space()), argUsages)) .build(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/Command.java b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/Command.java index 68410e923..46561bb0a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/Command.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/Command.java @@ -31,9 +31,7 @@ import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; - import net.kyori.adventure.text.Component; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/GenericChildCommand.java b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/GenericChildCommand.java index c92340c77..55db3cb78 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/GenericChildCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/GenericChildCommand.java @@ -34,13 +34,14 @@ import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; - import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -106,7 +107,7 @@ public void sendUsage(Sender sender) { TextComponent.Builder builder = Component.text() .append(Component.text('>', NamedTextColor.DARK_AQUA)) .append(Component.space()) - .append(Component.text(getName().toLowerCase(), NamedTextColor.GREEN)); + .append(Component.text(getName().toLowerCase(Locale.ROOT), NamedTextColor.GREEN)); if (getArgs() != null) { List argUsages = getArgs().stream() @@ -114,7 +115,7 @@ public void sendUsage(Sender sender) { .collect(Collectors.toList()); builder.append(Component.text(" - ", NamedTextColor.DARK_AQUA)) - .append(Component.join(Component.space(), argUsages)) + .append(Component.join(JoinConfiguration.separator(Component.space()), argUsages)) .build(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/GenericParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/GenericParentCommand.java index 510eee3cb..6b80a3305 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/GenericParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/GenericParentCommand.java @@ -38,6 +38,7 @@ import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.stream.Collectors; /** @@ -96,7 +97,7 @@ public List tabComplete(LuckPermsPlugin plugin, Sender sender, ArgumentL return TabCompleter.create() .at(0, CompletionSupplier.startsWith(() -> this.children.stream() .filter(s -> s.isAuthorized(sender, this.type)) - .map(s -> s.getName().toLowerCase()) + .map(s -> s.getName().toLowerCase(Locale.ROOT)) )) .from(1, partial -> this.children.stream() .filter(s -> s.isAuthorized(sender, this.type)) @@ -121,10 +122,10 @@ private void sendUsageDetailed(Sender sender, String label) { if (!subs.isEmpty()) { switch (this.type) { case USER: - Message.MAIN_COMMAND_USAGE_HEADER.send(sender, getName(), String.format("/%s user " + getName().toLowerCase(), label)); + Message.MAIN_COMMAND_USAGE_HEADER.send(sender, getName(), String.format("/%s user " + getName().toLowerCase(Locale.ROOT), label)); break; case GROUP: - Message.MAIN_COMMAND_USAGE_HEADER.send(sender, getName(), String.format("/%s group " + getName().toLowerCase(), label)); + Message.MAIN_COMMAND_USAGE_HEADER.send(sender, getName(), String.format("/%s group " + getName().toLowerCase(Locale.ROOT), label)); break; default: throw new AssertionError(this.type); diff --git a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ParentCommand.java index 0f41f74e6..99d8b494f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/ParentCommand.java @@ -33,11 +33,11 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @@ -86,44 +86,49 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, Argumen return; } - final String targetArgument = args.get(0); - I targetId = null; - if (this.type == Type.TAKES_ARGUMENT_FOR_TARGET) { - targetId = parseTarget(targetArgument, plugin, sender); + if (this.type == Type.TARGETED) { + final String targetArgument = args.get(0); + I targetId = parseTarget(targetArgument, plugin, sender); if (targetId == null) { return; } - } - ReentrantLock lock = getLockForTarget(targetId); - lock.lock(); - try { - T target = getTarget(targetId, plugin, sender); - if (target == null) { - return; + ReentrantLock lock = getLockForTarget(targetId); + lock.lock(); + try { + T target = getTarget(targetId, plugin, sender); + if (target == null) { + return; + } + + try { + sub.execute(plugin, sender, target, args.subList(this.type.minArgs, args.size()), label); + } catch (CommandException e) { + e.handle(sender, label, sub); + } + + cleanup(target, plugin); + } finally { + lock.unlock(); } - + } else { try { - sub.execute(plugin, sender, target, args.subList(this.type.minArgs, args.size()), label); + sub.execute(plugin, sender, null, args.subList(this.type.minArgs, args.size()), label); } catch (CommandException e) { e.handle(sender, label, sub); } - - cleanup(target, plugin); - } finally { - lock.unlock(); } } @Override public List tabComplete(LuckPermsPlugin plugin, Sender sender, ArgumentList args) { switch (this.type) { - case TAKES_ARGUMENT_FOR_TARGET: + case TARGETED: return TabCompleter.create() .at(0, CompletionSupplier.startsWith(() -> getTargets(plugin).stream())) .at(1, CompletionSupplier.startsWith(() -> getChildren().stream() .filter(s -> s.isAuthorized(sender)) - .map(s -> s.getName().toLowerCase()) + .map(s -> s.getName().toLowerCase(Locale.ROOT)) )) .from(2, partial -> getChildren().stream() .filter(s -> s.isAuthorized(sender)) @@ -133,11 +138,11 @@ public List tabComplete(LuckPermsPlugin plugin, Sender sender, ArgumentL .orElse(Collections.emptyList()) ) .complete(args); - case NO_TARGET_ARGUMENT: + case NOT_TARGETED: return TabCompleter.create() .at(0, CompletionSupplier.startsWith(() -> getChildren().stream() .filter(s -> s.isAuthorized(sender)) - .map(s -> s.getName().toLowerCase()) + .map(s -> s.getName().toLowerCase(Locale.ROOT)) )) .from(1, partial -> getChildren().stream() .filter(s -> s.isAuthorized(sender)) @@ -178,21 +183,31 @@ public boolean isAuthorized(Sender sender) { return getChildren().stream().anyMatch(sc -> sc.isAuthorized(sender)); } - protected abstract List getTargets(LuckPermsPlugin plugin); + protected List getTargets(LuckPermsPlugin plugin) { + throw new UnsupportedOperationException(); + } - protected abstract I parseTarget(String target, LuckPermsPlugin plugin, Sender sender); + protected I parseTarget(String target, LuckPermsPlugin plugin, Sender sender) { + throw new UnsupportedOperationException(); + } - protected abstract ReentrantLock getLockForTarget(I target); + protected ReentrantLock getLockForTarget(I target) { + throw new UnsupportedOperationException(); + } - protected abstract T getTarget(I target, LuckPermsPlugin plugin, Sender sender); + protected T getTarget(I target, LuckPermsPlugin plugin, Sender sender) { + throw new UnsupportedOperationException(); + } - protected abstract void cleanup(T t, LuckPermsPlugin plugin); + protected void cleanup(T t, LuckPermsPlugin plugin) { + throw new UnsupportedOperationException(); + } public enum Type { // e.g. /lp log sub-command.... - NO_TARGET_ARGUMENT(0), + NOT_TARGETED(0), // e.g. /lp user sub-command.... - TAKES_ARGUMENT_FOR_TARGET(1); + TARGETED(1); private final int cmdIndex; private final int minArgs; diff --git a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/SingleCommand.java b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/SingleCommand.java index ea9663f7c..81ac555b7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/abstraction/SingleCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/abstraction/SingleCommand.java @@ -32,12 +32,13 @@ import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; - import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import java.util.List; +import java.util.Locale; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -62,7 +63,7 @@ public void sendUsage(Sender sender, String label) { TextComponent.Builder builder = Component.text() .append(Component.text('>', NamedTextColor.DARK_AQUA)) .append(Component.space()) - .append(Component.text(getName().toLowerCase(), NamedTextColor.GREEN)); + .append(Component.text(getName().toLowerCase(Locale.ROOT), NamedTextColor.GREEN)); if (getArgs().isPresent()) { List argUsages = getArgs().get().stream() @@ -70,7 +71,7 @@ public void sendUsage(Sender sender, String label) { .collect(Collectors.toList()); builder.append(Component.text(" - ", NamedTextColor.DARK_AQUA)) - .append(Component.join(Component.space(), argUsages)) + .append(Component.join(JoinConfiguration.separator(Component.space()), argUsages)) .build(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/access/ArgumentPermissions.java b/common/src/main/java/me/lucko/luckperms/common/command/access/ArgumentPermissions.java index 1e04ce33d..005fd1e3a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/access/ArgumentPermissions.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/access/ArgumentPermissions.java @@ -25,9 +25,9 @@ package me.lucko.luckperms.common.command.access; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.cacheddata.type.PermissionCache; import me.lucko.luckperms.common.calculator.processor.DirectProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; @@ -38,8 +38,7 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextSet; import net.luckperms.api.util.Tristate; @@ -305,7 +304,7 @@ public static boolean checkGroup(LuckPermsPlugin plugin, Sender sender, String t } PermissionCache permissionData = user.getCachedData().getPermissionData(QueryOptionsImpl.DEFAULT_CONTEXTUAL.toBuilder().context(contextSet).build()); - TristateResult result = permissionData.checkPermission(Inheritance.key(targetGroupName), PermissionCheckEvent.Origin.INTERNAL); + TristateResult result = permissionData.checkPermission(Inheritance.key(targetGroupName), CheckOrigin.INTERNAL); return result.result() != Tristate.TRUE || result.processorClass() != DirectProcessor.class; } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/access/CommandPermission.java b/common/src/main/java/me/lucko/luckperms/common/command/access/CommandPermission.java index 606c4b790..fd1c1f91b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/access/CommandPermission.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/access/CommandPermission.java @@ -32,163 +32,173 @@ */ public enum CommandPermission { - SYNC("sync", Type.NONE), - INFO("info", Type.NONE), - EDITOR("editor", Type.NONE), - VERBOSE("verbose", Type.NONE), - VERBOSE_COMMAND_OTHERS("verbose.command.others", Type.NONE), - TREE("tree", Type.NONE), - SEARCH("search", Type.NONE), - IMPORT("import", Type.NONE), - EXPORT("export", Type.NONE), - RELOAD_CONFIG("reloadconfig", Type.NONE), - BULK_UPDATE("bulkupdate", Type.NONE), - APPLY_EDITS("applyedits", Type.NONE), - TRANSLATIONS("translations", Type.NONE), - - CREATE_GROUP("creategroup", Type.NONE), - DELETE_GROUP("deletegroup", Type.NONE), - LIST_GROUPS("listgroups", Type.NONE), - - CREATE_TRACK("createtrack", Type.NONE), - DELETE_TRACK("deletetrack", Type.NONE), - LIST_TRACKS("listtracks", Type.NONE), - - USER_INFO("info", Type.USER), - USER_PERM_INFO("permission.info", Type.USER), - USER_PERM_SET("permission.set", Type.USER), - USER_PERM_UNSET("permission.unset", Type.USER), - USER_PERM_SET_TEMP("permission.settemp", Type.USER), - USER_PERM_UNSET_TEMP("permission.unsettemp", Type.USER), - USER_PERM_CHECK("permission.check", Type.USER), - USER_PERM_CLEAR("permission.clear", Type.USER), - USER_PARENT_INFO("parent.info", Type.USER), - USER_PARENT_SET("parent.set", Type.USER), - USER_PARENT_SET_TRACK("parent.settrack", Type.USER), - USER_PARENT_ADD("parent.add", Type.USER), - USER_PARENT_REMOVE("parent.remove", Type.USER), - USER_PARENT_ADD_TEMP("parent.addtemp", Type.USER), - USER_PARENT_REMOVE_TEMP("parent.removetemp", Type.USER), - USER_PARENT_CLEAR("parent.clear", Type.USER), - USER_PARENT_CLEAR_TRACK("parent.cleartrack", Type.USER), - USER_PARENT_SWITCHPRIMARYGROUP("parent.switchprimarygroup", Type.USER), - USER_META_INFO("meta.info", Type.USER), - USER_META_SET("meta.set", Type.USER), - USER_META_UNSET("meta.unset", Type.USER), - USER_META_SET_TEMP("meta.settemp", Type.USER), - USER_META_UNSET_TEMP("meta.unsettemp", Type.USER), - USER_META_ADD_PREFIX("meta.addprefix", Type.USER), - USER_META_ADD_SUFFIX("meta.addsuffix", Type.USER), - USER_META_SET_PREFIX("meta.setprefix", Type.USER), - USER_META_SET_SUFFIX("meta.setsuffix", Type.USER), - USER_META_REMOVE_PREFIX("meta.removeprefix", Type.USER), - USER_META_REMOVE_SUFFIX("meta.removesuffix", Type.USER), - USER_META_ADD_TEMP_PREFIX("meta.addtempprefix", Type.USER), - USER_META_ADD_TEMP_SUFFIX("meta.addtempsuffix", Type.USER), - USER_META_SET_TEMP_PREFIX("meta.settempprefix", Type.USER), - USER_META_SET_TEMP_SUFFIX("meta.settempsuffix", Type.USER), - USER_META_REMOVE_TEMP_PREFIX("meta.removetempprefix", Type.USER), - USER_META_REMOVE_TEMP_SUFFIX("meta.removetempsuffix", Type.USER), - USER_META_CLEAR("meta.clear", Type.USER), - USER_EDITOR("editor", Type.USER), - USER_SHOW_TRACKS("showtracks", Type.USER), - USER_PROMOTE("promote", Type.USER), - USER_DEMOTE("demote", Type.USER), - USER_CLEAR("clear", Type.USER), - USER_CLONE("clone", Type.USER), - - GROUP_INFO("info", Type.GROUP), - GROUP_PERM_INFO("permission.info", Type.GROUP), - GROUP_PERM_SET("permission.set", Type.GROUP), - GROUP_PERM_UNSET("permission.unset", Type.GROUP), - GROUP_PERM_SET_TEMP("permission.settemp", Type.GROUP), - GROUP_PERM_UNSET_TEMP("permission.unsettemp", Type.GROUP), - GROUP_PERM_CHECK("permission.check", Type.GROUP), - GROUP_PERM_CLEAR("permission.clear", Type.GROUP), - GROUP_PARENT_INFO("parent.info", Type.GROUP), - GROUP_PARENT_SET("parent.set", Type.GROUP), - GROUP_PARENT_SET_TRACK("parent.settrack", Type.GROUP), - GROUP_PARENT_ADD("parent.add", Type.GROUP), - GROUP_PARENT_REMOVE("parent.remove", Type.GROUP), - GROUP_PARENT_ADD_TEMP("parent.addtemp", Type.GROUP), - GROUP_PARENT_REMOVE_TEMP("parent.removetemp", Type.GROUP), - GROUP_PARENT_CLEAR("parent.clear", Type.GROUP), - GROUP_PARENT_CLEAR_TRACK("parent.cleartrack", Type.GROUP), - GROUP_META_INFO("meta.info", Type.GROUP), - GROUP_META_SET("meta.set", Type.GROUP), - GROUP_META_UNSET("meta.unset", Type.GROUP), - GROUP_META_SET_TEMP("meta.settemp", Type.GROUP), - GROUP_META_UNSET_TEMP("meta.unsettemp", Type.GROUP), - GROUP_META_ADD_PREFIX("meta.addprefix", Type.GROUP), - GROUP_META_ADD_SUFFIX("meta.addsuffix", Type.GROUP), - GROUP_META_SET_PREFIX("meta.setprefix", Type.GROUP), - GROUP_META_SET_SUFFIX("meta.setsuffix", Type.GROUP), - GROUP_META_REMOVE_PREFIX("meta.removeprefix", Type.GROUP), - GROUP_META_REMOVE_SUFFIX("meta.removesuffix", Type.GROUP), - GROUP_META_ADD_TEMP_PREFIX("meta.addtempprefix", Type.GROUP), - GROUP_META_ADD_TEMP_SUFFIX("meta.addtempsuffix", Type.GROUP), - GROUP_META_SET_TEMP_PREFIX("meta.settempprefix", Type.GROUP), - GROUP_META_SET_TEMP_SUFFIX("meta.settempsuffix", Type.GROUP), - GROUP_META_REMOVE_TEMP_PREFIX("meta.removetempprefix", Type.GROUP), - GROUP_META_REMOVE_TEMP_SUFFIX("meta.removetempsuffix", Type.GROUP), - GROUP_META_CLEAR("meta.clear", Type.GROUP), - GROUP_EDITOR("editor", Type.GROUP), - GROUP_LIST_MEMBERS("listmembers", Type.GROUP), - GROUP_SHOW_TRACKS("showtracks", Type.GROUP), - GROUP_SET_WEIGHT("setweight", Type.GROUP), - GROUP_SET_DISPLAY_NAME("setdisplayname", Type.GROUP), - GROUP_CLEAR("clear", Type.GROUP), - GROUP_RENAME("rename", Type.GROUP), - GROUP_CLONE("clone", Type.GROUP), - - TRACK_INFO("info", Type.TRACK), - TRACK_EDITOR("editor", Type.TRACK), - TRACK_APPEND("append", Type.TRACK), - TRACK_INSERT("insert", Type.TRACK), - TRACK_REMOVE("remove", Type.TRACK), - TRACK_CLEAR("clear", Type.TRACK), - TRACK_RENAME("rename", Type.TRACK), - TRACK_CLONE("clone", Type.TRACK), - - LOG_RECENT("recent", Type.LOG), - LOG_USER_HISTORY("userhistory", Type.LOG), - LOG_GROUP_HISTORY("grouphistory", Type.LOG), - LOG_TRACK_HISTORY("trackhistory", Type.LOG), - LOG_SEARCH("search", Type.LOG), - LOG_NOTIFY("notify", Type.LOG), - - SPONGE_PERMISSION_INFO("permission.info", Type.SPONGE), - SPONGE_PERMISSION_SET("permission.set", Type.SPONGE), - SPONGE_PERMISSION_CLEAR("permission.clear", Type.SPONGE), - SPONGE_PARENT_INFO("parent.info", Type.SPONGE), - SPONGE_PARENT_ADD("parent.add", Type.SPONGE), - SPONGE_PARENT_REMOVE("parent.remove", Type.SPONGE), - SPONGE_PARENT_CLEAR("parent.clear", Type.SPONGE), - SPONGE_OPTION_INFO("option.info", Type.SPONGE), - SPONGE_OPTION_SET("option.set", Type.SPONGE), - SPONGE_OPTION_UNSET("option.unset", Type.SPONGE), - SPONGE_OPTION_CLEAR("option.clear", Type.SPONGE); + SYNC("sync", Type.NONE, true), + INFO("info", Type.NONE, true), + EDITOR("editor", Type.NONE, true), + VERBOSE("verbose", Type.NONE, true), + VERBOSE_COMMAND_OTHERS("verbose.command.others", Type.NONE, false), + TREE("tree", Type.NONE, true), + SEARCH("search", Type.NONE, true), + IMPORT("import", Type.NONE, false), + EXPORT("export", Type.NONE, true), + RELOAD_CONFIG("reloadconfig", Type.NONE, true), + BULK_UPDATE("bulkupdate", Type.NONE, false), + APPLY_EDITS("applyedits", Type.NONE, false), + TRUST_EDITOR("trusteditor", Type.NONE, false), + TRANSLATIONS("translations", Type.NONE, true), + + CREATE_GROUP("creategroup", Type.NONE, false), + DELETE_GROUP("deletegroup", Type.NONE, false), + LIST_GROUPS("listgroups", Type.NONE, true), + + CREATE_TRACK("createtrack", Type.NONE, false), + DELETE_TRACK("deletetrack", Type.NONE, false), + LIST_TRACKS("listtracks", Type.NONE, true), + + USER_INFO("info", Type.USER, true), + USER_PERM_INFO("permission.info", Type.USER, true), + USER_PERM_SET("permission.set", Type.USER, false), + USER_PERM_UNSET("permission.unset", Type.USER, false), + USER_PERM_SET_TEMP("permission.settemp", Type.USER, false), + USER_PERM_UNSET_TEMP("permission.unsettemp", Type.USER, false), + USER_PERM_CHECK("permission.check", Type.USER, true), + USER_PERM_CLEAR("permission.clear", Type.USER, false), + USER_PARENT_INFO("parent.info", Type.USER, true), + USER_PARENT_SET("parent.set", Type.USER, false), + USER_PARENT_SET_TRACK("parent.settrack", Type.USER, false), + USER_PARENT_ADD("parent.add", Type.USER, false), + USER_PARENT_REMOVE("parent.remove", Type.USER, false), + USER_PARENT_ADD_TEMP("parent.addtemp", Type.USER, false), + USER_PARENT_REMOVE_TEMP("parent.removetemp", Type.USER, false), + USER_PARENT_CLEAR("parent.clear", Type.USER, false), + USER_PARENT_CLEAR_TRACK("parent.cleartrack", Type.USER, false), + USER_PARENT_SWITCHPRIMARYGROUP("parent.switchprimarygroup", Type.USER, false), + USER_META_INFO("meta.info", Type.USER, true), + USER_META_SET("meta.set", Type.USER, false), + USER_META_UNSET("meta.unset", Type.USER, false), + USER_META_SET_TEMP("meta.settemp", Type.USER, false), + USER_META_UNSET_TEMP("meta.unsettemp", Type.USER, false), + USER_META_ADD_PREFIX("meta.addprefix", Type.USER, false), + USER_META_ADD_SUFFIX("meta.addsuffix", Type.USER, false), + USER_META_SET_PREFIX("meta.setprefix", Type.USER, false), + USER_META_SET_SUFFIX("meta.setsuffix", Type.USER, false), + USER_META_REMOVE_PREFIX("meta.removeprefix", Type.USER, false), + USER_META_REMOVE_SUFFIX("meta.removesuffix", Type.USER, false), + USER_META_ADD_TEMP_PREFIX("meta.addtempprefix", Type.USER, false), + USER_META_ADD_TEMP_SUFFIX("meta.addtempsuffix", Type.USER, false), + USER_META_SET_TEMP_PREFIX("meta.settempprefix", Type.USER, false), + USER_META_SET_TEMP_SUFFIX("meta.settempsuffix", Type.USER, false), + USER_META_REMOVE_TEMP_PREFIX("meta.removetempprefix", Type.USER, false), + USER_META_REMOVE_TEMP_SUFFIX("meta.removetempsuffix", Type.USER, false), + USER_META_CLEAR("meta.clear", Type.USER, false), + USER_EDITOR("editor", Type.USER, true), + USER_SHOW_TRACKS("showtracks", Type.USER, true), + USER_PROMOTE("promote", Type.USER, false), + USER_DEMOTE("demote", Type.USER, false), + USER_CLEAR("clear", Type.USER, false), + USER_CLONE("clone", Type.USER, false), + + GROUP_INFO("info", Type.GROUP, true), + GROUP_PERM_INFO("permission.info", Type.GROUP, true), + GROUP_PERM_SET("permission.set", Type.GROUP, false), + GROUP_PERM_UNSET("permission.unset", Type.GROUP, false), + GROUP_PERM_SET_TEMP("permission.settemp", Type.GROUP, false), + GROUP_PERM_UNSET_TEMP("permission.unsettemp", Type.GROUP, false), + GROUP_PERM_CHECK("permission.check", Type.GROUP, true), + GROUP_PERM_CLEAR("permission.clear", Type.GROUP, false), + GROUP_PARENT_INFO("parent.info", Type.GROUP, true), + GROUP_PARENT_SET("parent.set", Type.GROUP, false), + GROUP_PARENT_SET_TRACK("parent.settrack", Type.GROUP, false), + GROUP_PARENT_ADD("parent.add", Type.GROUP, false), + GROUP_PARENT_REMOVE("parent.remove", Type.GROUP, false), + GROUP_PARENT_ADD_TEMP("parent.addtemp", Type.GROUP, false), + GROUP_PARENT_REMOVE_TEMP("parent.removetemp", Type.GROUP, false), + GROUP_PARENT_CLEAR("parent.clear", Type.GROUP, false), + GROUP_PARENT_CLEAR_TRACK("parent.cleartrack", Type.GROUP, false), + GROUP_META_INFO("meta.info", Type.GROUP, true), + GROUP_META_SET("meta.set", Type.GROUP, false), + GROUP_META_UNSET("meta.unset", Type.GROUP, false), + GROUP_META_SET_TEMP("meta.settemp", Type.GROUP, false), + GROUP_META_UNSET_TEMP("meta.unsettemp", Type.GROUP, false), + GROUP_META_ADD_PREFIX("meta.addprefix", Type.GROUP, false), + GROUP_META_ADD_SUFFIX("meta.addsuffix", Type.GROUP, false), + GROUP_META_SET_PREFIX("meta.setprefix", Type.GROUP, false), + GROUP_META_SET_SUFFIX("meta.setsuffix", Type.GROUP, false), + GROUP_META_REMOVE_PREFIX("meta.removeprefix", Type.GROUP, false), + GROUP_META_REMOVE_SUFFIX("meta.removesuffix", Type.GROUP, false), + GROUP_META_ADD_TEMP_PREFIX("meta.addtempprefix", Type.GROUP, false), + GROUP_META_ADD_TEMP_SUFFIX("meta.addtempsuffix", Type.GROUP, false), + GROUP_META_SET_TEMP_PREFIX("meta.settempprefix", Type.GROUP, false), + GROUP_META_SET_TEMP_SUFFIX("meta.settempsuffix", Type.GROUP, false), + GROUP_META_REMOVE_TEMP_PREFIX("meta.removetempprefix", Type.GROUP, false), + GROUP_META_REMOVE_TEMP_SUFFIX("meta.removetempsuffix", Type.GROUP, false), + GROUP_META_CLEAR("meta.clear", Type.GROUP, false), + GROUP_EDITOR("editor", Type.GROUP, true), + GROUP_LIST_MEMBERS("listmembers", Type.GROUP, true), + GROUP_SHOW_TRACKS("showtracks", Type.GROUP, true), + GROUP_SET_WEIGHT("setweight", Type.GROUP, false), + GROUP_SET_DISPLAY_NAME("setdisplayname", Type.GROUP, false), + GROUP_CLEAR("clear", Type.GROUP, false), + GROUP_RENAME("rename", Type.GROUP, false), + GROUP_CLONE("clone", Type.GROUP, false), + + TRACK_INFO("info", Type.TRACK, true), + TRACK_EDITOR("editor", Type.TRACK, true), + TRACK_APPEND("append", Type.TRACK, false), + TRACK_INSERT("insert", Type.TRACK, false), + TRACK_REMOVE("remove", Type.TRACK, false), + TRACK_CLEAR("clear", Type.TRACK, false), + TRACK_RENAME("rename", Type.TRACK, false), + TRACK_CLONE("clone", Type.TRACK, false), + + LOG_RECENT("recent", Type.LOG, true), + LOG_USER_HISTORY("userhistory", Type.LOG, true), + LOG_GROUP_HISTORY("grouphistory", Type.LOG, true), + LOG_TRACK_HISTORY("trackhistory", Type.LOG, true), + LOG_SEARCH("search", Type.LOG, true), + LOG_NOTIFY("notify", Type.LOG, true), + + SPONGE_PERMISSION_INFO("permission.info", Type.SPONGE, true), + SPONGE_PERMISSION_SET("permission.set", Type.SPONGE, false), + SPONGE_PERMISSION_CLEAR("permission.clear", Type.SPONGE, false), + SPONGE_PARENT_INFO("parent.info", Type.SPONGE, true), + SPONGE_PARENT_ADD("parent.add", Type.SPONGE, false), + SPONGE_PARENT_REMOVE("parent.remove", Type.SPONGE, false), + SPONGE_PARENT_CLEAR("parent.clear", Type.SPONGE, false), + SPONGE_OPTION_INFO("option.info", Type.SPONGE, true), + SPONGE_OPTION_SET("option.set", Type.SPONGE, false), + SPONGE_OPTION_UNSET("option.unset", Type.SPONGE, false), + SPONGE_OPTION_CLEAR("option.clear", Type.SPONGE, false); public static final String ROOT = "luckperms."; private final String node; + private final String permission; private final Type type; + private final boolean readOnly; - CommandPermission(String node, Type type) { + CommandPermission(String node, Type type, boolean readOnly) { this.type = type; + this.readOnly = readOnly; if (type == Type.NONE) { - this.node = ROOT + node; + this.node = node; } else { - this.node = ROOT + type.getTag() + "." + node; + this.node = type.getTag() + "." + node; } + + this.permission = ROOT + this.node; } - public String getPermission() { + public String getNode() { return this.node; } + public String getPermission() { + return this.permission; + } + public boolean isAuthorized(Sender sender) { return sender.hasPermission(this); } @@ -197,6 +207,10 @@ public Type getType() { return this.type; } + public boolean isReadOnly() { + return this.readOnly; + } + public enum Type { NONE(null), diff --git a/common/src/main/java/me/lucko/luckperms/common/command/spec/Argument.java b/common/src/main/java/me/lucko/luckperms/common/command/spec/Argument.java index f9788a3a6..cf089893e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/spec/Argument.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/spec/Argument.java @@ -26,15 +26,15 @@ package me.lucko.luckperms.common.command.spec; import me.lucko.luckperms.common.locale.Message; - import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; public class Argument { private final String name; private final boolean required; - private final Component description; + private final TranslatableComponent description; - Argument(String name, boolean required, Component description) { + Argument(String name, boolean required, TranslatableComponent description) { this.name = name; this.required = required; this.description = description; @@ -48,7 +48,7 @@ public boolean isRequired() { return this.required; } - public Component getDescription() { + public TranslatableComponent getDescription() { return this.description; } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/spec/CommandSpec.java b/common/src/main/java/me/lucko/luckperms/common/command/spec/CommandSpec.java index ebf9f665c..ecebaed73 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/spec/CommandSpec.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/spec/CommandSpec.java @@ -26,11 +26,12 @@ package me.lucko.luckperms.common.command.spec; import me.lucko.luckperms.common.util.ImmutableCollectors; - import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; import java.util.Arrays; import java.util.List; +import java.util.Locale; /** * An enumeration of the command defintion/usage messages used in the plugin. @@ -85,9 +86,11 @@ public enum CommandSpec { TRANSLATIONS("/%s translations", arg("install", false) ), - APPLY_EDITS("/%s applyedits [target]", - arg("code", true), - arg("target", false) + APPLY_EDITS("/%s applyedits ", + arg("code", true) + ), + TRUST_EDITOR("/%s trusteditor ", + arg("id", true) ), CREATE_GROUP("/%s creategroup ", @@ -98,8 +101,9 @@ public enum CommandSpec { DELETE_GROUP("/%s deletegroup ", arg("name", true) ), - LIST_GROUPS("/%s listgroups"), - + LIST_GROUPS("/%s listgroups", + arg("page", false) + ), CREATE_TRACK("/%s createtrack ", arg("name", true) ), @@ -128,7 +132,8 @@ public enum CommandSpec { GROUP_INFO, GROUP_LISTMEMBERS( - arg("page", false) + arg("page", false), + arg("context...", false) ), GROUP_SETWEIGHT( arg("weight", true) @@ -414,7 +419,7 @@ public enum CommandSpec { this.args = args.length == 0 ? null : Arrays.stream(args) .map(builder -> { String key = builder.id.replace(".", "").replace(' ', '-'); - Component description = Component.translatable("luckperms.usage." + key() + ".argument." + key); + TranslatableComponent description = Component.translatable("luckperms.usage." + key() + ".argument." + key); return new Argument(builder.name, builder.required, description); }) .collect(ImmutableCollectors.toList()); @@ -424,7 +429,7 @@ public enum CommandSpec { this(null, args); } - public Component description() { + public TranslatableComponent description() { return Component.translatable("luckperms.usage." + this.key() + ".description"); } @@ -437,7 +442,7 @@ public List args() { } public String key() { - return name().toLowerCase().replace('_', '-'); + return name().toLowerCase(Locale.ROOT).replace('_', '-'); } private static PartialArgument arg(String id, String name, boolean required) { diff --git a/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/CompletionSupplier.java b/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/CompletionSupplier.java index b4f5f3a9a..f755e74c2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/CompletionSupplier.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/CompletionSupplier.java @@ -25,6 +25,8 @@ package me.lucko.luckperms.common.command.tabcomplete; +import me.lucko.luckperms.common.util.Predicates; + import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -46,7 +48,7 @@ static CompletionSupplier startsWith(Collection strings) { } static CompletionSupplier startsWith(Supplier> stringsSupplier) { - return partial -> stringsSupplier.get().filter(TabCompleter.startsWithIgnoreCase(partial)).collect(Collectors.toList()); + return partial -> stringsSupplier.get().filter(Predicates.startsWithIgnoreCase(partial)).collect(Collectors.toList()); } static CompletionSupplier contains(String... strings) { @@ -58,7 +60,7 @@ static CompletionSupplier contains(Collection strings) { } static CompletionSupplier contains(Supplier> stringsSupplier) { - return partial -> stringsSupplier.get().filter(TabCompleter.containsIgnoreCase(partial)).collect(Collectors.toList()); + return partial -> stringsSupplier.get().filter(Predicates.containsIgnoreCase(partial)).collect(Collectors.toList()); } List supplyCompletions(String partial); diff --git a/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/TabCompleter.java b/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/TabCompleter.java index 07a9b5e4a..4506023d6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/TabCompleter.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/TabCompleter.java @@ -30,7 +30,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Predicate; /** * Utility for computing tab completion results @@ -98,22 +97,4 @@ private List getCompletions(int position, String partial) { return this.suppliers.getOrDefault(position, CompletionSupplier.EMPTY).supplyCompletions(partial); } - static Predicate startsWithIgnoreCase(String prefix) { - return string -> { - if (string.length() < prefix.length()) { - return false; - } - return string.regionMatches(true, 0, prefix, 0, prefix.length()); - }; - } - - static Predicate containsIgnoreCase(String substring) { - return string -> { - if (string.length() < substring.length()) { - return false; - } - return string.toLowerCase().contains(substring.toLowerCase()); - }; - } - } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/TabCompletions.java b/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/TabCompletions.java index f2bbdab6a..08e112a00 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/TabCompletions.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/tabcomplete/TabCompletions.java @@ -26,16 +26,16 @@ package me.lucko.luckperms.common.command.tabcomplete; import com.google.common.base.Splitter; - import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.treeview.PermissionRegistry; import me.lucko.luckperms.common.treeview.TreeNode; - +import me.lucko.luckperms.common.util.Predicates; import net.luckperms.api.context.ImmutableContextSet; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; @@ -57,7 +57,7 @@ public TabCompletions(LuckPermsPlugin plugin) { this.permissions = partial -> { PermissionRegistry cache = plugin.getPermissionRegistry(); - String start = partial.toLowerCase(); + String start = partial.toLowerCase(Locale.ROOT); List parts = new ArrayList<>(Splitter.on('.').splitToList(start)); TreeNode root = cache.getRootNode(); @@ -89,7 +89,7 @@ public TabCompletions(LuckPermsPlugin plugin) { } return root.getChildren().get().keySet().stream() - .filter(TabCompleter.startsWithIgnoreCase(incomplete)) + .filter(Predicates.startsWithIgnoreCase(incomplete)) .map(s -> String.join(".", parts) + "." + s) .collect(Collectors.toList()); }; @@ -111,7 +111,7 @@ public TabCompletions(LuckPermsPlugin plugin) { String value = partial.substring(index + 1).trim(); Set potentialValues = potentialContexts.getValues(key); return potentialValues.stream() - .filter(TabCompleter.startsWithIgnoreCase(value)) + .filter(Predicates.startsWithIgnoreCase(value)) .map(s -> key + "=" + s) .collect(Collectors.toList()); }; diff --git a/common/src/main/java/me/lucko/luckperms/common/command/utils/ArgumentList.java b/common/src/main/java/me/lucko/luckperms/common/command/utils/ArgumentList.java index 016d7da04..2c199a725 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/utils/ArgumentList.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/utils/ArgumentList.java @@ -26,27 +26,25 @@ package me.lucko.luckperms.common.command.utils; import com.google.common.collect.ForwardingList; - import me.lucko.luckperms.common.command.abstraction.CommandException; import me.lucko.luckperms.common.commands.user.UserParentCommand; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; -import me.lucko.luckperms.common.context.contextset.MutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.MutableContextSetImpl; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.DurationParser; - import net.luckperms.api.context.Context; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.TemporaryNodeMergeStrategy; - import org.checkerframework.checker.nullness.qual.NonNull; import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.UUID; import java.util.function.Predicate; @@ -101,7 +99,7 @@ public int getIntOrDefault(int index, int defaultValue) { } public String getLowercase(int index, Predicate test) throws ArgumentException.DetailedUsage { - String arg = get(index).toLowerCase(); + String arg = get(index).toLowerCase(Locale.ROOT); if (!test.test(arg)) { throw new ArgumentException.DetailedUsage(); } @@ -188,7 +186,7 @@ public Optional getTemporaryModifierAndRemove(int in } private static TemporaryNodeMergeStrategy parseTemporaryModifier(String s) { - switch (s.toLowerCase()) { + switch (s.toLowerCase(Locale.ROOT)) { case "accumulate": return TemporaryNodeMergeStrategy.ADD_NEW_DURATION_TO_EXISTING; case "replace": diff --git a/common/src/main/java/me/lucko/luckperms/common/command/utils/SortType.java b/common/src/main/java/me/lucko/luckperms/common/command/utils/SortType.java index 7a08413f6..1cc2b5913 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/utils/SortType.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/utils/SortType.java @@ -25,6 +25,8 @@ package me.lucko.luckperms.common.command.utils; +import java.util.Locale; + public enum SortType { PRIORITY, @@ -32,6 +34,6 @@ public enum SortType { @Override public String toString() { - return name().toLowerCase(); + return name().toLowerCase(Locale.ROOT); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/command/utils/StorageAssistant.java b/common/src/main/java/me/lucko/luckperms/common/command/utils/StorageAssistant.java index 7cc180d56..8e4bdbba1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/command/utils/StorageAssistant.java +++ b/common/src/main/java/me/lucko/luckperms/common/command/utils/StorageAssistant.java @@ -37,6 +37,7 @@ import me.lucko.luckperms.common.sender.Sender; import java.util.Optional; +import java.util.concurrent.CompletableFuture; /** * Utility methods for saving users, groups and tracks. @@ -88,40 +89,28 @@ public static void save(User user, Sender sender, LuckPermsPlugin plugin) { } } - public static void save(Group group, Sender sender, LuckPermsPlugin plugin) { + public static CompletableFuture save(Group group, Sender sender, LuckPermsPlugin plugin) { try { plugin.getStorage().saveGroup(group).get(); } catch (Exception e) { plugin.getLogger().warn("Error whilst saving group", e); Message.GROUP_SAVE_ERROR.send(sender, group); - return; + return failedFuture(e); } - plugin.getGroupManager().invalidateAllGroupCaches(); - plugin.getUserManager().invalidateAllUserCaches(); - - Optional messagingService = plugin.getMessagingService(); - if (messagingService.isPresent() && plugin.getConfiguration().get(ConfigKeys.AUTO_PUSH_UPDATES)) { - messagingService.get().getUpdateBuffer().request(); - } + return invalidateCachesAndPushUpdates(plugin); } - public static void save(Track track, Sender sender, LuckPermsPlugin plugin) { + public static CompletableFuture save(Track track, Sender sender, LuckPermsPlugin plugin) { try { plugin.getStorage().saveTrack(track).get(); } catch (Exception e) { plugin.getLogger().warn("Error whilst saving track", e); Message.TRACK_SAVE_ERROR.send(sender, track.getName()); - return; + return failedFuture(e); } - plugin.getGroupManager().invalidateAllGroupCaches(); - plugin.getUserManager().invalidateAllUserCaches(); - - Optional messagingService = plugin.getMessagingService(); - if (messagingService.isPresent() && plugin.getConfiguration().get(ConfigKeys.AUTO_PUSH_UPDATES)) { - messagingService.get().getUpdateBuffer().request(); - } + return invalidateCachesAndPushUpdates(plugin); } public static void save(PermissionHolder holder, Sender sender, LuckPermsPlugin plugin) { @@ -136,4 +125,21 @@ public static void save(PermissionHolder holder, Sender sender, LuckPermsPlugin } } + public static CompletableFuture invalidateCachesAndPushUpdates(LuckPermsPlugin plugin) { + plugin.getGroupManager().invalidateAllGroupCaches(); + plugin.getUserManager().invalidateAllUserCaches(); + + Optional messagingService = plugin.getMessagingService(); + if (messagingService.isPresent() && plugin.getConfiguration().get(ConfigKeys.AUTO_PUSH_UPDATES)) { + return messagingService.get().getUpdateBuffer().request(); + } else { + return CompletableFuture.completedFuture(null); + } + } + + private static CompletableFuture failedFuture(Throwable ex) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(ex); + return future; + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/CommandMeta.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/CommandMeta.java index 400a7232c..62ccd9eca 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/CommandMeta.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/CommandMeta.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.commands.generic.meta; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.command.abstraction.GenericChildCommand; import me.lucko.luckperms.common.command.abstraction.GenericParentCommand; import me.lucko.luckperms.common.command.spec.CommandSpec; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaAddChatMeta.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaAddChatMeta.java index 66eb7e8ee..eb23817ef 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaAddChatMeta.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaAddChatMeta.java @@ -40,13 +40,13 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.ChatMetaType; import java.util.List; +import java.util.Locale; public class MetaAddChatMeta extends GenericChildCommand { @@ -99,7 +99,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ Message.ADD_CHATMETA_SUCCESS.send(sender, target, this.type, meta, priority, context); LoggedAction.build().source(sender).target(target) - .description("meta" , "add" + this.type.name().toLowerCase(), priority, meta, context) + .description("meta" , "add" + this.type.name().toLowerCase(Locale.ROOT), priority, meta, context) .build().submit(plugin, sender); StorageAssistant.save(target, sender, plugin); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaAddTempChatMeta.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaAddTempChatMeta.java index a2050e03f..d46843299 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaAddTempChatMeta.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaAddTempChatMeta.java @@ -41,7 +41,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; @@ -50,6 +49,7 @@ import java.time.Duration; import java.util.List; +import java.util.Locale; public class MetaAddTempChatMeta extends GenericChildCommand { @@ -107,7 +107,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ Message.ADD_TEMP_CHATMETA_SUCCESS.send(sender, target, this.type, meta, priority, duration, context); LoggedAction.build().source(sender).target(target) - .description("meta" , "addtemp" + this.type.name().toLowerCase(), priority, meta, duration, context) + .description("meta" , "addtemp" + this.type.name().toLowerCase(Locale.ROOT), priority, meta, duration, context) .build().submit(plugin, sender); StorageAssistant.save(target, sender, plugin); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaClear.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaClear.java index 6f8a66b05..5836484d4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaClear.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaClear.java @@ -40,12 +40,12 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; import java.util.List; +import java.util.Locale; public class MetaClear extends GenericChildCommand { public MetaClear() { @@ -61,7 +61,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ NodeType type = null; if (!args.isEmpty()) { - String typeId = args.get(0).toLowerCase(); + String typeId = args.get(0).toLowerCase(Locale.ROOT); if (typeId.equals("any") || typeId.equals("all") || typeId.equals("*")) { type = NodeType.META_OR_CHAT_META; } @@ -104,7 +104,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ } int changed = before - target.normalData().size(); - Message.META_CLEAR_SUCCESS.send(sender, target, type.name().toLowerCase(), context, changed); + Message.META_CLEAR_SUCCESS.send(sender, target, type.name().toLowerCase(Locale.ROOT), context, changed); LoggedAction.build().source(sender).target(target) .description("meta", "clear", context) diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaInfo.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaInfo.java index b0a8e2951..f2cf7c2df 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaInfo.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaInfo.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.commands.generic.meta; import com.google.common.collect.Maps; - import me.lucko.luckperms.common.command.abstraction.GenericChildCommand; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -39,7 +38,6 @@ import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.ChatMetaNode; @@ -120,7 +118,7 @@ public int compare(Map.Entry> o1, Map.Entr if (result != 0) { return result; } - return NodeWithContextComparator.normal().compare(o1.getValue(), o2.getValue()); + return NodeWithContextComparator.ascending().compare(o1.getValue(), o2.getValue()); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaRemoveChatMeta.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaRemoveChatMeta.java index 6d3e2889d..a4ee32c1b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaRemoveChatMeta.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaRemoveChatMeta.java @@ -40,13 +40,13 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.ChatMetaType; import java.util.List; +import java.util.Locale; public class MetaRemoveChatMeta extends GenericChildCommand { @@ -100,7 +100,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ Message.BULK_REMOVE_CHATMETA_SUCCESS.send(sender, target, this.type, priority, context); LoggedAction.build().source(sender).target(target) - .description("meta" , "remove" + this.type.name().toLowerCase(), priority, "*", context) + .description("meta" , "remove" + this.type.name().toLowerCase(Locale.ROOT), priority, "*", context) .build().submit(plugin, sender); StorageAssistant.save(target, sender, plugin); @@ -113,7 +113,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ Message.REMOVE_CHATMETA_SUCCESS.send(sender, target, this.type, meta, priority, context); LoggedAction.build().source(sender).target(target) - .description("meta" , "remove" + this.type.name().toLowerCase(), priority, meta, context) + .description("meta" , "remove" + this.type.name().toLowerCase(Locale.ROOT), priority, meta, context) .build().submit(plugin, sender); StorageAssistant.save(target, sender, plugin); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaRemoveTempChatMeta.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaRemoveTempChatMeta.java index cd8951103..f19daaf24 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaRemoveTempChatMeta.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaRemoveTempChatMeta.java @@ -40,13 +40,13 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.ChatMetaType; import java.util.List; +import java.util.Locale; public class MetaRemoveTempChatMeta extends GenericChildCommand { @@ -100,7 +100,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ Message.BULK_REMOVE_TEMP_CHATMETA_SUCCESS.send(sender, target, this.type, priority, context); LoggedAction.build().source(sender).target(target) - .description("meta" , "removetemp" + this.type.name().toLowerCase(), priority, "*", context) + .description("meta" , "removetemp" + this.type.name().toLowerCase(Locale.ROOT), priority, "*", context) .build().submit(plugin, sender); StorageAssistant.save(target, sender, plugin); @@ -113,7 +113,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ Message.REMOVE_TEMP_CHATMETA_SUCCESS.send(sender, target, this.type, meta, priority, context); LoggedAction.build().source(sender).target(target) - .description("meta" , "removetemp" + this.type.name().toLowerCase(), priority, meta, context) + .description("meta" , "removetemp" + this.type.name().toLowerCase(Locale.ROOT), priority, meta, context) .build().submit(plugin, sender); StorageAssistant.save(target, sender, plugin); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSet.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSet.java index a423dfc6e..7e81b7d45 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSet.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSet.java @@ -41,7 +41,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; @@ -66,6 +65,11 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ String value = args.get(1); MutableContextSet context = args.getContextOrDefault(2, plugin); + if (key.isEmpty()) { + Message.INVALID_META_KEY_EMPTY.send(sender); + return; + } + if (ArgumentPermissions.checkContext(plugin, sender, permission, context) || ArgumentPermissions.checkGroup(plugin, sender, target, context) || ArgumentPermissions.checkArguments(plugin, sender, permission, key)) { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetChatMeta.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetChatMeta.java index 4f115f64c..55d050b01 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetChatMeta.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetChatMeta.java @@ -43,13 +43,13 @@ import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.ChatMetaType; import java.util.List; +import java.util.Locale; import java.util.OptionalInt; public class MetaSetChatMeta extends GenericChildCommand { @@ -134,7 +134,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ Message.ADD_CHATMETA_SUCCESS.send(sender, target, this.type, meta, priority, context); LoggedAction.build().source(sender).target(target) - .description("meta" , "set" + this.type.name().toLowerCase(), priority, meta, context) + .description("meta" , "set" + this.type.name().toLowerCase(Locale.ROOT), priority, meta, context) .build().submit(plugin, sender); StorageAssistant.save(target, sender, plugin); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetTemp.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetTemp.java index 77b807fe4..8962e64b9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetTemp.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetTemp.java @@ -42,7 +42,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; @@ -71,6 +70,11 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ TemporaryNodeMergeStrategy modifier = args.getTemporaryModifierAndRemove(3).orElseGet(() -> plugin.getConfiguration().get(ConfigKeys.TEMPORARY_ADD_BEHAVIOUR)); MutableContextSet context = args.getContextOrDefault(3, plugin); + if (key.isEmpty()) { + Message.INVALID_META_KEY_EMPTY.send(sender); + return; + } + if (ArgumentPermissions.checkContext(plugin, sender, permission, context) || ArgumentPermissions.checkGroup(plugin, sender, target, context) || ArgumentPermissions.checkArguments(plugin, sender, permission, key)) { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetTempChatMeta.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetTempChatMeta.java index 964049533..d7ce523a9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetTempChatMeta.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaSetTempChatMeta.java @@ -44,7 +44,6 @@ import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; @@ -53,6 +52,7 @@ import java.time.Duration; import java.util.List; +import java.util.Locale; import java.util.OptionalInt; public class MetaSetTempChatMeta extends GenericChildCommand { @@ -146,7 +146,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ Message.ADD_TEMP_CHATMETA_SUCCESS.send(sender, target, this.type, meta, priority, duration, context); LoggedAction.build().source(sender).target(target) - .description("meta" , "settemp" + this.type.name().toLowerCase(), priority, meta, duration, context) + .description("meta" , "settemp" + this.type.name().toLowerCase(Locale.ROOT), priority, meta, duration, context) .build().submit(plugin, sender); StorageAssistant.save(target, sender, plugin); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaUnset.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaUnset.java index 67269172b..05f2f5ff1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaUnset.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaUnset.java @@ -40,7 +40,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; @@ -62,6 +61,11 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ String key = args.get(0); MutableContextSet context = args.getContextOrDefault(1, plugin); + if (key.isEmpty()) { + Message.INVALID_META_KEY_EMPTY.send(sender); + return; + } + if (ArgumentPermissions.checkContext(plugin, sender, permission, context) || ArgumentPermissions.checkGroup(plugin, sender, target, context) || ArgumentPermissions.checkArguments(plugin, sender, permission, key)) { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaUnsetTemp.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaUnsetTemp.java index e7050c801..d600a27e9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaUnsetTemp.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/meta/MetaUnsetTemp.java @@ -40,7 +40,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; @@ -62,6 +61,11 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ String key = args.get(0); MutableContextSet context = args.getContextOrDefault(1, plugin); + if (key.isEmpty()) { + Message.INVALID_META_KEY_EMPTY.send(sender); + return; + } + if (ArgumentPermissions.checkContext(plugin, sender, permission, context) || ArgumentPermissions.checkGroup(plugin, sender, target, context) || ArgumentPermissions.checkArguments(plugin, sender, permission, key)) { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderClear.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderClear.java index ad3d9afb3..422492ccf 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderClear.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderClear.java @@ -41,7 +41,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataType; 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 bca8c8f44..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 @@ -30,7 +30,7 @@ import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; @@ -42,7 +42,7 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.common.webeditor.WebEditorRequest; - +import me.lucko.luckperms.common.webeditor.WebEditorSession; import net.luckperms.api.node.Node; import java.util.ArrayList; @@ -84,8 +84,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, T target, ArgumentLis Message.EDITOR_START.send(sender); - WebEditorRequest.generate(holders, Collections.emptyList(), sender, label, plugin) - .createSession(plugin, sender); + WebEditorSession.create(holders, Collections.emptyList(), sender, label, plugin).open(); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderShowTracks.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderShowTracks.java index 9dee07c70..fdfb363d7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderShowTracks.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/other/HolderShowTracks.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.commands.generic.other; import com.google.common.collect.Maps; - import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -37,15 +36,15 @@ import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.model.Track; +import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - -import net.kyori.adventure.text.Component; import net.luckperms.api.node.Node; import net.luckperms.api.node.types.InheritanceNode; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; @@ -71,7 +70,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, T target, ArgumentLis return; } - List> lines = new ArrayList<>(); + List> lines = new ArrayList<>(); if (target.getType() == HolderType.USER) { // if the holder is a user, we want to query parent groups for tracks @@ -87,13 +86,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, T target, ArgumentLis .collect(Collectors.toList()); for (Track track : tracks) { - Component line = Component.text() - .append(Message.formatContextSetBracketed(node.getContexts(), Component.empty())) - .append(Component.newline()) - .append(Message.formatTrackPath(track.getGroups(), groupName)) - .build(); - - lines.add(Maps.immutableEntry(track, line)); + lines.add(Maps.immutableEntry(track, node)); } } } else { @@ -101,10 +94,11 @@ public void execute(LuckPermsPlugin plugin, Sender sender, T target, ArgumentLis String groupName = ((Group) target).getName(); List tracks = plugin.getTrackManager().getAll().values().stream() .filter(t -> t.containsGroup(groupName)) + .sorted(Comparator.comparing(Track::getName)) .collect(Collectors.toList()); for (Track track : tracks) { - lines.add(Maps.immutableEntry(track, Message.formatTrackPath(track.getGroups(), groupName))); + lines.add(Maps.immutableEntry(track, Inheritance.builder(groupName).build())); } } @@ -114,8 +108,10 @@ public void execute(LuckPermsPlugin plugin, Sender sender, T target, ArgumentLis } Message.LIST_TRACKS.send(sender, target); - for (Map.Entry line : lines) { - Message.LIST_TRACKS_ENTRY.send(sender, line.getKey().getName(), line.getValue()); + for (Map.Entry line : lines) { + Track track = line.getKey(); + InheritanceNode node = line.getValue(); + Message.LIST_TRACKS_ENTRY.send(sender, track.getName(), node.getContexts(), Message.formatTrackPath(track.getGroups(), node.getGroupName())); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/CommandParent.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/CommandParent.java index 0d4a78db5..b1593ac45 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/CommandParent.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/CommandParent.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.commands.generic.parent; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.command.abstraction.GenericChildCommand; import me.lucko.luckperms.common.command.abstraction.GenericParentCommand; import me.lucko.luckperms.common.command.spec.CommandSpec; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentAdd.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentAdd.java index 2d101e61e..fa294551f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentAdd.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentAdd.java @@ -43,7 +43,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentAddTemp.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentAddTemp.java index bdb77c54a..d73b7e709 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentAddTemp.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentAddTemp.java @@ -44,7 +44,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; @@ -83,7 +82,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ return; } - if (group.getName().equalsIgnoreCase(target.getObjectName())) { + if (group.getName().equalsIgnoreCase(target.getIdentifier().getName())) { Message.ALREADY_TEMP_INHERITS.send(sender, target, group, context); return; } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClear.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClear.java index cb9bc83ac..29e8623ce 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClear.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClear.java @@ -40,7 +40,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClearTrack.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClearTrack.java index 1a20d7226..dcca0b5c5 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClearTrack.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentClearTrack.java @@ -44,12 +44,12 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; import java.util.List; +import java.util.Locale; public class ParentClearTrack extends GenericChildCommand { public ParentClearTrack() { @@ -63,7 +63,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ return; } - final String trackName = args.get(0).toLowerCase(); + final String trackName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.TRACK_NAME_TEST.test(trackName)) { Message.TRACK_INVALID_ENTRY.send(sender, trackName); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentInfo.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentInfo.java index d7b2eb470..a6f3c6b86 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentInfo.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentInfo.java @@ -40,7 +40,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Iterators; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.node.types.InheritanceNode; import java.util.Collections; @@ -116,6 +115,6 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ } // fallback to priority - return NodeWithContextComparator.reverse().compare(o1, o2); + return NodeWithContextComparator.descending().compare(o1, o2); }; } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemove.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemove.java index 208d4330d..9d43604c0 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemove.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemove.java @@ -46,7 +46,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.kyori.adventure.text.Component; import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemoveTemp.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemoveTemp.java index 377caaa29..c3f473d6e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemoveTemp.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentRemoveTemp.java @@ -42,7 +42,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.kyori.adventure.text.Component; import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentSet.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentSet.java index afe376266..32f43445e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentSet.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentSet.java @@ -45,7 +45,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentSetTrack.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentSetTrack.java index 36b63606b..d12d913d9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentSetTrack.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/ParentSetTrack.java @@ -44,12 +44,12 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; import java.util.List; +import java.util.Locale; public class ParentSetTrack extends GenericChildCommand { public ParentSetTrack() { @@ -63,7 +63,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ return; } - final String trackName = args.get(0).toLowerCase(); + final String trackName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.TRACK_NAME_TEST.test(trackName)) { Message.TRACK_INVALID_ENTRY.send(sender, trackName); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/UserSwitchPrimaryGroup.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/UserSwitchPrimaryGroup.java index 483235fc7..d174b7dea 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/UserSwitchPrimaryGroup.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/parent/UserSwitchPrimaryGroup.java @@ -35,7 +35,7 @@ import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.command.utils.StorageAssistant; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.PermissionHolder; @@ -45,12 +45,12 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeEqualityPredicate; import java.util.List; +import java.util.Locale; public class UserSwitchPrimaryGroup extends GenericChildCommand { public UserSwitchPrimaryGroup() { @@ -74,9 +74,9 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ Message.USER_PRIMARYGROUP_WARN_OPTION.send(sender, opt); } - Group group = plugin.getGroupManager().getIfLoaded(args.get(0).toLowerCase()); + Group group = plugin.getGroupManager().getIfLoaded(args.get(0).toLowerCase(Locale.ROOT)); if (group == null) { - Message.DOES_NOT_EXIST.send(sender, args.get(0).toLowerCase()); + Message.DOES_NOT_EXIST.send(sender, args.get(0).toLowerCase(Locale.ROOT)); return; } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/CommandPermission.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/CommandPermission.java index b1c9baa36..64d2c850d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/CommandPermission.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/CommandPermission.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.commands.generic.permission; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.command.abstraction.GenericChildCommand; import me.lucko.luckperms.common.command.abstraction.GenericParentCommand; import me.lucko.luckperms.common.command.spec.CommandSpec; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionCheck.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionCheck.java index f56f69b0a..469083eb9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionCheck.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionCheck.java @@ -25,8 +25,8 @@ package me.lucko.luckperms.common.commands.generic.permission; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; import me.lucko.luckperms.common.command.abstraction.CommandException; import me.lucko.luckperms.common.command.abstraction.GenericChildCommand; import me.lucko.luckperms.common.command.access.ArgumentPermissions; @@ -43,8 +43,7 @@ import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.kyori.adventure.text.Component; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.PermissionHolder.Identifier; @@ -72,6 +71,10 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ } String node = args.get(0); + if (node.isEmpty()) { + Message.INVALID_PERMISSION_EMPTY.send(sender); + return; + } // accumulate nodes List own = new ArrayList<>(); @@ -123,29 +126,13 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ // perform a "real" check QueryOptions queryOptions = target.getQueryOptions(); - TristateResult checkResult = target.getCachedData().getPermissionData(queryOptions).checkPermission(node, PermissionCheckEvent.Origin.INTERNAL); + TristateResult checkResult = target.getCachedData().getPermissionData(queryOptions).checkPermission(node, CheckOrigin.INTERNAL); Tristate result = checkResult.result(); - String processor; - String cause; + String processor = checkResult.processorClassFriendly(); + Node cause = checkResult.node(); ImmutableContextSet context = queryOptions.context(); - if (result != Tristate.UNDEFINED) { - Class processorClass = checkResult.processorClass(); - if (processorClass.getName().startsWith("me.lucko.luckperms.")) { - String simpleName = processorClass.getSimpleName(); - String platform = processorClass.getName().split("\\.")[3]; - processor = platform + "." + simpleName; - } else { - processor = processorClass.getName(); - } - - cause = checkResult.cause(); - } else { - processor = null; - cause = null; - } - // send results Message.PERMISSION_CHECK_RESULT.send(sender, node, result, processor, cause, context); } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionClear.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionClear.java index ee6b880a1..31d953f58 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionClear.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionClear.java @@ -40,7 +40,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionInfo.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionInfo.java index 714461a51..291b92eb0 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionInfo.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionInfo.java @@ -39,7 +39,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Iterators; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeType; @@ -116,6 +115,6 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ } // fallback to priority - return NodeWithContextComparator.reverse().compare(o1, o2); + return NodeWithContextComparator.descending().compare(o1, o2); }; } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionSet.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionSet.java index 7ec065ed9..86d465077 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionSet.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionSet.java @@ -38,15 +38,17 @@ import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.node.factory.NodeBuilders; +import me.lucko.luckperms.common.node.utils.ShorthandParseException; +import me.lucko.luckperms.common.node.utils.ShorthandParser; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; 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 java.util.List; @@ -67,7 +69,8 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ MutableContextSet context = args.getContextOrDefault(2, plugin); if (node.isEmpty()) { - Message.PERMISSION_INVALID_ENTRY_EMPTY.send(sender); + Message.INVALID_PERMISSION_EMPTY.send(sender); + return; } if (ArgumentPermissions.checkContext(plugin, sender, permission, context) || @@ -86,6 +89,13 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ } } + if (builtNode instanceof PermissionNode) { + ShorthandParseException shorthandError = ShorthandParser.checkParse(builtNode.getKey()); + if (shorthandError != null) { + Message.SHORTHAND_PARSE_ERROR.send(sender, builtNode.getKey(), shorthandError.getMessage()); + } + } + DataMutateResult result = target.setNode(DataType.NORMAL, builtNode, true); if (result.wasSuccessful()) { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionSetTemp.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionSetTemp.java index 36ac9785c..255b7849b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionSetTemp.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionSetTemp.java @@ -39,16 +39,18 @@ import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.node.factory.NodeBuilders; +import me.lucko.luckperms.common.node.utils.ShorthandParseException; +import me.lucko.luckperms.common.node.utils.ShorthandParser; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; import net.luckperms.api.model.data.TemporaryNodeMergeStrategy; import net.luckperms.api.node.Node; import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.node.types.PermissionNode; import java.time.Duration; import java.util.List; @@ -72,7 +74,8 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ MutableContextSet context = args.getContextOrDefault(3, plugin); if (node.isEmpty()) { - Message.PERMISSION_INVALID_ENTRY_EMPTY.send(sender); + Message.INVALID_PERMISSION_EMPTY.send(sender); + return; } if (ArgumentPermissions.checkContext(plugin, sender, permission, context) || @@ -91,6 +94,13 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ } } + if (builtNode instanceof PermissionNode) { + ShorthandParseException shorthandError = ShorthandParser.checkParse(builtNode.getKey()); + if (shorthandError != null) { + Message.SHORTHAND_PARSE_ERROR.send(sender, builtNode.getKey(), shorthandError.getMessage()); + } + } + DataMutateResult.WithMergedNode result = target.setNode(DataType.NORMAL, builtNode, modifier); if (result.getResult().wasSuccessful()) { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionUnset.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionUnset.java index 544d29e71..05d639984 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionUnset.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionUnset.java @@ -41,7 +41,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; @@ -66,7 +65,8 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ MutableContextSet context = args.getContextOrDefault(1, plugin); if (node.isEmpty()) { - Message.PERMISSION_INVALID_ENTRY_EMPTY.send(sender); + Message.INVALID_PERMISSION_EMPTY.send(sender); + return; } if (ArgumentPermissions.checkContext(plugin, sender, permission, context) || diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionUnsetTemp.java b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionUnsetTemp.java index 2ba38edce..f1371fb44 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionUnsetTemp.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/generic/permission/PermissionUnsetTemp.java @@ -41,7 +41,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; @@ -69,7 +68,8 @@ public void execute(LuckPermsPlugin plugin, Sender sender, PermissionHolder targ MutableContextSet context = args.getContextOrDefault(fromIndex, plugin); if (node.isEmpty()) { - Message.PERMISSION_INVALID_ENTRY_EMPTY.send(sender); + Message.INVALID_PERMISSION_EMPTY.send(sender); + return; } if (ArgumentPermissions.checkContext(plugin, sender, permission, context) || diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/CreateGroup.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/CreateGroup.java index b7592bac8..5c3c9a88d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/CreateGroup.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/CreateGroup.java @@ -40,12 +40,13 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.kyori.adventure.text.Component; import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.model.data.DataType; +import java.util.Locale; + public class CreateGroup extends SingleCommand { public CreateGroup() { super(CommandSpec.CREATE_GROUP, "CreateGroup", CommandPermission.CREATE_GROUP, Predicates.notInRange(1, 3)); @@ -58,7 +59,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St return; } - String groupName = args.get(0).toLowerCase(); + String groupName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.GROUP_NAME_TEST.test(groupName)) { Message.GROUP_INVALID_ENTRY.send(sender, groupName); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/DeleteGroup.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/DeleteGroup.java index 66628c6aa..2de521a43 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/DeleteGroup.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/DeleteGroup.java @@ -26,28 +26,40 @@ package me.lucko.luckperms.common.commands.group; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateBuilder; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateField; +import me.lucko.luckperms.common.bulkupdate.DataType; +import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; import me.lucko.luckperms.common.command.abstraction.SingleCommand; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; +import me.lucko.luckperms.common.command.tabcomplete.CompletionSupplier; import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.tabcomplete.TabCompletions; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.manager.group.GroupManager; +import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.cause.DeletionCause; import java.util.List; +import java.util.Locale; +import java.util.Optional; public class DeleteGroup extends SingleCommand { public DeleteGroup() { - super(CommandSpec.DELETE_GROUP, "DeleteGroup", CommandPermission.DELETE_GROUP, Predicates.not(1)); + super(CommandSpec.DELETE_GROUP, "DeleteGroup", CommandPermission.DELETE_GROUP, Predicates.notInRange(1, 2)); } @Override @@ -57,7 +69,11 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St return; } - String groupName = args.get(0).toLowerCase(); + String groupName = args.get(0).toLowerCase(Locale.ROOT); + if (!DataConstraints.GROUP_NAME_TEST.test(groupName)) { + Message.GROUP_INVALID_ENTRY.send(sender, groupName); + return; + } if (groupName.equalsIgnoreCase(GroupManager.DEFAULT_GROUP_NAME)) { Message.DELETE_GROUP_ERROR_DEFAULT.send(sender); @@ -89,13 +105,35 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St .description("delete") .build().submit(plugin, sender); - plugin.getSyncTaskBuffer().request(); + if (!args.remove("--update-parent-lists")) { + plugin.getSyncTaskBuffer().request(); + } else { + // the group is now deleted, proceed to remove its representing inheritance nodes + BulkUpdate operation = BulkUpdateBuilder.create() + .trackStatistics(false) + .dataType(DataType.ALL) + .action(DeleteAction.create()) + .filter(BulkUpdateField.PERMISSION, Comparison.EQUAL, Inheritance.key(groupName)) + .build(); + plugin.getStorage().applyBulkUpdate(operation).whenCompleteAsync((v, ex) -> { + if (ex != null) { + ex.printStackTrace(); + } + + plugin.getSyncTaskBuffer().requestDirectly(); // sync regardless of failure state + Optional messagingService = plugin.getMessagingService(); + if (messagingService.isPresent() && plugin.getConfiguration().get(ConfigKeys.AUTO_PUSH_UPDATES)) { + messagingService.get().getUpdateBuffer().request(); + } + }, plugin.getBootstrap().getScheduler().async()); + } } @Override public List tabComplete(LuckPermsPlugin plugin, Sender sender, ArgumentList args) { return TabCompleter.create() .at(0, TabCompletions.groups(plugin)) + .at(1, CompletionSupplier.startsWith("--update-parent-lists")) .complete(args); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupClone.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupClone.java index 2461bb742..01f487326 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupClone.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupClone.java @@ -38,10 +38,11 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.model.data.DataType; +import java.util.Locale; + public class GroupClone extends ChildCommand { public GroupClone() { super(CommandSpec.GROUP_CLONE, "clone", CommandPermission.GROUP_CLONE, Predicates.not(1)); @@ -54,7 +55,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Group target, Argumen return; } - String newGroupName = args.get(0).toLowerCase(); + String newGroupName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.GROUP_NAME_TEST.test(newGroupName)) { Message.GROUP_INVALID_ENTRY.send(sender, newGroupName); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupInfo.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupInfo.java index 21566678d..e99c1f02d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupInfo.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupInfo.java @@ -25,7 +25,7 @@ package me.lucko.luckperms.common.commands.group; -import me.lucko.luckperms.common.cacheddata.type.MetaCache; +import me.lucko.luckperms.common.cacheddata.type.MonitoredMetaCache; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -36,12 +36,12 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.node.Node; import net.luckperms.api.node.types.InheritanceNode; import net.luckperms.api.query.QueryOptions; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -60,35 +60,32 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Group target, Argumen Message.GROUP_INFO_GENERAL.send(sender, target.getName(), target.getPlainDisplayName(), target.getWeight()); - List parents = target.normalData().inheritanceAsSortedSet().stream() + Map> parents = target.normalData().inheritanceAsSortedSet().stream() .filter(Node::getValue) - .filter(n -> !n.hasExpiry()) - .collect(Collectors.toList()); + .collect(Collectors.groupingBy(Node::hasExpiry, Collectors.toList())); - List tempParents = target.normalData().inheritanceAsSortedSet().stream() - .filter(Node::getValue) - .filter(Node::hasExpiry) - .collect(Collectors.toList()); + List temporaryParents = parents.getOrDefault(true, Collections.emptyList()); + List permanentParents = parents.getOrDefault(false, Collections.emptyList()); - if (!parents.isEmpty()) { + if (!permanentParents.isEmpty()) { Message.INFO_PARENT_HEADER.send(sender); - for (InheritanceNode node : parents) { + for (InheritanceNode node : permanentParents) { Message.INFO_PARENT_NODE_ENTRY.send(sender, node); } } - if (!tempParents.isEmpty()) { + if (!temporaryParents.isEmpty()) { Message.INFO_TEMP_PARENT_HEADER.send(sender); - for (InheritanceNode node : tempParents) { + for (InheritanceNode node : temporaryParents) { Message.INFO_PARENT_TEMPORARY_NODE_ENTRY.send(sender, node); } } QueryOptions queryOptions = plugin.getContextManager().getStaticQueryOptions(); - MetaCache data = target.getCachedData().getMetaData(queryOptions); - String prefix = data.getPrefix(MetaCheckEvent.Origin.INTERNAL); - String suffix = data.getSuffix(MetaCheckEvent.Origin.INTERNAL); - Map> meta = data.getMeta(MetaCheckEvent.Origin.INTERNAL); + MonitoredMetaCache data = target.getCachedData().getMetaData(queryOptions); + String prefix = data.getPrefix(CheckOrigin.INTERNAL).result(); + String suffix = data.getSuffix(CheckOrigin.INTERNAL).result(); + Map> meta = data.getMeta(CheckOrigin.INTERNAL); Message.GROUP_INFO_CONTEXTUAL_DATA.send(sender, prefix, suffix, meta); } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupListMembers.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupListMembers.java index 396c2065f..96ac218a6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupListMembers.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupListMembers.java @@ -26,12 +26,14 @@ package me.lucko.luckperms.common.commands.group; import com.google.common.collect.Maps; - import me.lucko.luckperms.common.cache.LoadingMap; import me.lucko.luckperms.common.command.abstraction.ChildCommand; +import me.lucko.luckperms.common.command.abstraction.CommandException; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; +import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; +import me.lucko.luckperms.common.command.tabcomplete.TabCompletions; import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; @@ -47,7 +49,7 @@ import me.lucko.luckperms.common.storage.misc.NodeEntry; import me.lucko.luckperms.common.util.Iterators; import me.lucko.luckperms.common.util.Predicates; - +import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.types.InheritanceNode; import java.util.ArrayList; @@ -59,11 +61,11 @@ public class GroupListMembers extends ChildCommand { public GroupListMembers() { - super(CommandSpec.GROUP_LISTMEMBERS, "listmembers", CommandPermission.GROUP_LIST_MEMBERS, Predicates.notInRange(0, 1)); + super(CommandSpec.GROUP_LISTMEMBERS, "listmembers", CommandPermission.GROUP_LIST_MEMBERS, Predicates.notInRange(0, 2)); } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Group target, ArgumentList args, String label) { + public void execute(LuckPermsPlugin plugin, Sender sender, Group target, ArgumentList args, String label) throws CommandException { if (ArgumentPermissions.checkViewPerms(plugin, sender, getPermission().get(), target)) { Message.COMMAND_NO_PERMISSION.send(sender); return; @@ -72,11 +74,13 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Group target, Argumen InheritanceNode node = Inheritance.builder(target.getName()).build(); ConstraintNodeMatcher matcher = StandardNodeMatchers.key(node); int page = args.getIntOrDefault(0, 1); + ImmutableContextSet context = args.getContextOrEmpty(1); Message.SEARCH_SEARCHING_MEMBERS.send(sender, target.getName()); List> matchedUsers = plugin.getStorage().searchUserNodes(matcher).join().stream() .filter(n -> n.getNode().getValue()) + .filter(n -> context.isEmpty() || n.getNode().getContexts().isSatisfiedBy(context)) .collect(Collectors.toList()); // special handling for default group @@ -94,6 +98,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Group target, Argumen List> matchedGroups = plugin.getStorage().searchGroupNodes(matcher).join().stream() .filter(n -> n.getNode().getValue()) + .filter(n -> context.isEmpty() || n.getNode().getContexts().isSatisfiedBy(context)) .collect(Collectors.toList()); int users = matchedUsers.size(); @@ -113,7 +118,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Group target, Argumen private static > void sendResult(Sender sender, List> results, Function lookupFunction, Message.Args3 headerMessage, HolderType holderType, String label, int page) { results = new ArrayList<>(results); - results.sort(NodeEntryComparator.normal()); + results.sort(NodeEntryComparator.ascending()); int pageIndex = page - 1; List>> pages = Iterators.divideIterable(results, 15); @@ -136,4 +141,11 @@ private static > void sendResult(Sender sender, List tabComplete(LuckPermsPlugin plugin, Sender sender, ArgumentList args) { + return TabCompleter.create() + .from(1, TabCompletions.contexts(plugin)) + .complete(args); + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupParentCommand.java index 8262a55e0..507d30395 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupParentCommand.java @@ -27,7 +27,6 @@ import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.command.abstraction.Command; import me.lucko.luckperms.common.command.abstraction.ParentCommand; import me.lucko.luckperms.common.command.spec.CommandSpec; @@ -46,6 +45,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -60,7 +60,7 @@ public class GroupParentCommand extends ParentCommand { .build(key -> new ReentrantLock()); public GroupParentCommand() { - super(CommandSpec.GROUP, "Group", Type.TAKES_ARGUMENT_FOR_TARGET, ImmutableList.>builder() + super(CommandSpec.GROUP, "Group", Type.TARGETED, ImmutableList.>builder() .add(new GroupInfo()) .add(new CommandPermission<>(HolderType.GROUP)) .add(new CommandParent<>(HolderType.GROUP)) @@ -83,7 +83,7 @@ protected String parseTarget(String target, LuckPermsPlugin plugin, Sender sende if (group != null) { return group.getName(); } else { - return target.toLowerCase(); + return target.toLowerCase(Locale.ROOT); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupRename.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupRename.java index 11081760c..57cf03349 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupRename.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupRename.java @@ -26,27 +26,38 @@ package me.lucko.luckperms.common.commands.group; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateBuilder; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateField; +import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; +import me.lucko.luckperms.common.command.tabcomplete.CompletionSupplier; +import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.command.utils.StorageAssistant; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.kyori.adventure.text.Component; import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.event.cause.DeletionCause; import net.luckperms.api.model.data.DataType; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + public class GroupRename extends ChildCommand { public GroupRename() { - super(CommandSpec.GROUP_RENAME, "rename", CommandPermission.GROUP_RENAME, Predicates.not(1)); + super(CommandSpec.GROUP_RENAME, "rename", CommandPermission.GROUP_RENAME, Predicates.notInRange(1, 2)); } @Override @@ -56,7 +67,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Group target, Argumen return; } - String newGroupName = args.get(0).toLowerCase(); + String newGroupName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.GROUP_NAME_TEST.test(newGroupName)) { Message.GROUP_INVALID_ENTRY.send(sender, newGroupName); return; @@ -92,6 +103,33 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Group target, Argumen .description("rename", newGroup.getName()) .build().submit(plugin, sender); - StorageAssistant.save(newGroup, sender, plugin); + StorageAssistant.save(newGroup, sender, plugin) + .thenCompose((v) -> { + if (args.remove("--update-parent-lists")) { + // the group is now renamed, proceed to update its representing inheritance nodes + BulkUpdate operation = BulkUpdateBuilder.create() + .trackStatistics(false) + .dataType(me.lucko.luckperms.common.bulkupdate.DataType.ALL) + .action(UpdateAction.of(BulkUpdateField.PERMISSION, Inheritance.key(newGroupName))) + .filter(BulkUpdateField.PERMISSION, Comparison.EQUAL, Inheritance.key(target.getName())) + .build(); + return plugin.getStorage().applyBulkUpdate(operation); + } else { + return CompletableFuture.completedFuture(v); + } + }).whenCompleteAsync((v, ex) -> { + if (ex != null) { + ex.printStackTrace(); + } + + plugin.getSyncTaskBuffer().requestDirectly(); + }, plugin.getBootstrap().getScheduler().async()); + } + + @Override + public List tabComplete(LuckPermsPlugin plugin, Sender sender, ArgumentList args) { + return TabCompleter.create() + .at(1, CompletionSupplier.startsWith("--update-parent-lists")) + .complete(args); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupSetDisplayName.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupSetDisplayName.java index fe00b961b..479638630 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupSetDisplayName.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupSetDisplayName.java @@ -41,7 +41,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; @@ -64,6 +63,11 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Group target, Argumen String name = args.get(0); ImmutableContextSet context = args.getContextOrDefault(1, plugin).immutableCopy(); + if (name.isEmpty()) { + Message.INVALID_DISPLAY_NAME_EMPTY.send(sender); + return; + } + String previousName = target.normalData().nodesInContext(context).stream() .filter(NodeType.DISPLAY_NAME::matches) .map(NodeType.DISPLAY_NAME::cast) diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupSetWeight.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupSetWeight.java index bb7fd53e3..7193510f4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupSetWeight.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/GroupSetWeight.java @@ -39,7 +39,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeType; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/group/ListGroups.java b/common/src/main/java/me/lucko/luckperms/common/commands/group/ListGroups.java index 6f77368eb..5937eae0d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/group/ListGroups.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/group/ListGroups.java @@ -30,17 +30,20 @@ import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.Iterators; import me.lucko.luckperms.common.util.Predicates; +import java.util.Collection; import java.util.List; import java.util.stream.Collectors; public class ListGroups extends SingleCommand { public ListGroups() { - super(CommandSpec.LIST_GROUPS, "ListGroups", CommandPermission.LIST_GROUPS, Predicates.alwaysFalse()); + super(CommandSpec.LIST_GROUPS, "ListGroups", CommandPermission.LIST_GROUPS, Predicates.notInRange(0, 1)); } @Override @@ -53,15 +56,29 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St return; } - Message.GROUPS_LIST.send(sender); - plugin.getGroupManager().getAll().values().stream() - .sorted((o1, o2) -> { + int page = args.getIntOrDefault(0, 1); + int pageIndex = page - 1; + + List groups = plugin.getGroupManager().getAll().values().stream().sorted((o1, o2) -> { int i = Integer.compare(o2.getWeight().orElse(0), o1.getWeight().orElse(0)); return i != 0 ? i : o1.getName().compareToIgnoreCase(o2.getName()); - }) - .forEach(group -> { - List tracks = plugin.getTrackManager().getAll().values().stream().filter(t -> t.containsGroup(group)).map(Track::getName).collect(Collectors.toList()); - Message.GROUPS_LIST_ENTRY.send(sender, group, group.getWeight().orElse(0), tracks); - }); + }).collect(Collectors.toList()); + + List> pages = Iterators.divideIterable(groups, 8); + + if (pageIndex < 0 || pageIndex >= pages.size()) { + page = 1; + pageIndex = 0; + } + + Message.SEARCH_SHOWING_GROUPS.send(sender, page, pages.size(), groups.size()); + Message.GROUPS_LIST.send(sender); + + Collection allTracks = plugin.getTrackManager().getAll().values(); + + for (Group group : pages.get(pageIndex)) { + List tracks = allTracks.stream().filter(t -> t.containsGroup(group)).map(Track::getName).collect(Collectors.toList()); + Message.GROUPS_LIST_ENTRY.send(sender, group, group.getWeight().orElse(0), tracks); + } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogGroupHistory.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogGroupHistory.java index 6b92bb910..a44ef10e6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogGroupHistory.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogGroupHistory.java @@ -25,24 +25,26 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.tabcomplete.TabCompletions; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; +import java.util.Locale; -public class LogGroupHistory extends ChildCommand { +public class LogGroupHistory extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogGroupHistory() { @@ -50,44 +52,38 @@ public LogGroupHistory() { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { - String group = args.get(0).toLowerCase(); + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { + String group = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.GROUP_NAME_TEST.test(group)) { Message.GROUP_INVALID_ENTRY.send(sender, group); return; } - Paginated content = new Paginated<>(log.getGroupHistory(group)); - - int page = args.getIntOrDefault(1, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - showLog(page, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), sender, content); + int page = args.getIntOrDefault(1, 1); + if (page < 1) { + Message.LOG_INVALID_PAGE_RANGE.send(sender, 1); + return; } - } - private static void showLog(int page, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, page); + LogPage log = plugin.getStorage().getLogPage(ActionFilters.group(group), pageParams).join(); + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); + + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } - if (page == Integer.MIN_VALUE) { - page = maxPage; - } - - if (page < 1 || page > maxPage) { + if (page > maxPage) { Message.LOG_INVALID_PAGE_RANGE.send(sender, maxPage); return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); + List> entries = log.getNumberedContent(); String name = entries.stream().findAny().get().value().getTarget().getName(); Message.LOG_HISTORY_GROUP_HEADER.send(sender, name, page, maxPage); - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogNotify.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogNotify.java index abffbd33e..9361d3535 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogNotify.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogNotify.java @@ -25,26 +25,24 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.node.types.Permission; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; import java.util.Optional; import java.util.UUID; -public class LogNotify extends ChildCommand { +public class LogNotify extends ChildCommand { private static final String IGNORE_NODE = "luckperms.log.notify.ignoring"; public LogNotify() { @@ -84,7 +82,7 @@ private static void setIgnoring(LuckPermsPlugin plugin, UUID uuid, boolean state } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { if (sender.isConsole()) { Message.LOG_NOTIFY_CONSOLE.send(sender); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogParentCommand.java index 5f701f60b..a608ddc3a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogParentCommand.java @@ -26,23 +26,13 @@ package me.lucko.luckperms.common.commands.log; import com.google.common.collect.ImmutableList; - -import me.lucko.luckperms.common.actionlog.Log; import me.lucko.luckperms.common.command.abstraction.Command; import me.lucko.luckperms.common.command.abstraction.ParentCommand; import me.lucko.luckperms.common.command.spec.CommandSpec; -import me.lucko.luckperms.common.locale.Message; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.sender.Sender; - -import java.util.List; -import java.util.concurrent.locks.ReentrantLock; - -public class LogParentCommand extends ParentCommand { - private final ReentrantLock lock = new ReentrantLock(); +public class LogParentCommand extends ParentCommand { public LogParentCommand() { - super(CommandSpec.LOG, "Log", Type.NO_TARGET_ARGUMENT, ImmutableList.>builder() + super(CommandSpec.LOG, "Log", Type.NOT_TARGETED, ImmutableList.>builder() .add(new LogRecent()) .add(new LogSearch()) .add(new LogNotify()) @@ -52,38 +42,4 @@ public LogParentCommand() { .build() ); } - - @Override - protected ReentrantLock getLockForTarget(Void target) { - return this.lock; // all commands target the same log, so we share a lock between all "targets" - } - - @Override - protected Log getTarget(Void target, LuckPermsPlugin plugin, Sender sender) { - Log log = plugin.getStorage().getLog().join(); - - if (log == null) { - Message.LOG_LOAD_ERROR.send(sender); - } - - return log; - } - - @Override - protected void cleanup(Log log, LuckPermsPlugin plugin) { - - } - - @Override - protected List getTargets(LuckPermsPlugin plugin) { - // should never be called if we specify Type.NO_TARGET_ARGUMENT in the constructor - throw new UnsupportedOperationException(); - } - - @Override - protected Void parseTarget(String target, LuckPermsPlugin plugin, Sender sender) { - // should never be called if we specify Type.NO_TARGET_ARGUMENT in the constructor - throw new UnsupportedOperationException(); - } - } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogRecent.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogRecent.java index 3e400cd8b..39a795349 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogRecent.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogRecent.java @@ -25,22 +25,23 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; import java.util.UUID; -public class LogRecent extends ChildCommand { +public class LogRecent extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogRecent() { @@ -48,50 +49,48 @@ public LogRecent() { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { - if (args.isEmpty()) { - // No page or user - Paginated content = new Paginated<>(log.getContent()); - showLog(content.getMaxPages(ENTRIES_PER_PAGE), false, sender, content); - return; - } + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { + int page = 1; + UUID uuid = null; - int page = args.getIntOrDefault(0, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - Paginated content = new Paginated<>(log.getContent()); - showLog(page, false, sender, content); - return; + if (!args.isEmpty()) { + int pageNo = args.getIntOrDefault(0, Integer.MIN_VALUE); + if (pageNo != Integer.MIN_VALUE) { + page = pageNo; + } else { + uuid = args.getUserTarget(0, plugin, sender); + if (uuid == null) { + return; + } + + pageNo = args.getIntOrDefault(1, Integer.MIN_VALUE); + if (pageNo != Integer.MIN_VALUE) { + page = pageNo; + } + } } - // User and possibly page - UUID uuid = args.getUserTarget(0, plugin, sender); - if (uuid == null) { + if (page < 1) { + Message.LOG_INVALID_PAGE_RANGE.send(sender, 1); return; } - Paginated content = new Paginated<>(log.getContent(uuid)); - page = args.getIntOrDefault(1, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - showLog(page, true, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), true, sender, content); - } - } + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, page); + LogPage log = plugin.getStorage().getLogPage(uuid == null ? ActionFilters.all() : ActionFilters.source(uuid), pageParams).join(); - private static void showLog(int page, boolean specificUser, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } - if (page < 1 || page > maxPage) { + if (page > maxPage) { Message.LOG_INVALID_PAGE_RANGE.send(sender, maxPage); return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); - if (specificUser) { + List> entries = log.getNumberedContent(); + if (uuid != null) { String name = entries.stream().findAny().get().value().getSource().getName(); if (name.contains("@")) { name = name.split("@")[0]; @@ -101,8 +100,9 @@ private static void showLog(int page, boolean specificUser, Sender sender, Pagin Message.LOG_RECENT_HEADER.send(sender, page, maxPage); } - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogSearch.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogSearch.java index 4d82ff01b..0266bad48 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogSearch.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogSearch.java @@ -25,21 +25,22 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; -public class LogSearch extends ChildCommand { +public class LogSearch extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogSearch() { @@ -47,8 +48,8 @@ public LogSearch() { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { - int page = Integer.MIN_VALUE; + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { + int page = 1; if (args.size() > 1) { try { page = Integer.parseInt(args.get(args.size() - 1)); @@ -59,36 +60,32 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList } final String query = String.join(" ", args); - Paginated content = new Paginated<>(log.getSearch(query)); - if (page != Integer.MIN_VALUE) { - showLog(page, query, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), query, sender, content); + if (page < 1) { + Message.LOG_INVALID_PAGE_RANGE.send(sender, 1); + return; } - } - private static void showLog(int page, String query, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, page); + LogPage log = plugin.getStorage().getLogPage(ActionFilters.search(query), pageParams).join(); + + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } - if (page == Integer.MIN_VALUE) { - page = maxPage; - } - - if (page < 1 || page > maxPage) { + if (page > maxPage) { Message.LOG_INVALID_PAGE_RANGE.send(sender, maxPage); return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); + List> entries = log.getNumberedContent(); Message.LOG_SEARCH_HEADER.send(sender, query, page, maxPage); - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogTrackHistory.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogTrackHistory.java index a7a6de3f2..954289408 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogTrackHistory.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogTrackHistory.java @@ -25,24 +25,26 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.tabcomplete.TabCompletions; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; +import java.util.Locale; -public class LogTrackHistory extends ChildCommand { +public class LogTrackHistory extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogTrackHistory() { @@ -50,44 +52,37 @@ public LogTrackHistory() { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { - String track = args.get(0).toLowerCase(); + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { + String track = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.TRACK_NAME_TEST.test(track)) { Message.TRACK_INVALID_ENTRY.send(sender, track); return; } - - Paginated content = new Paginated<>(log.getTrackHistory(track)); - - int page = args.getIntOrDefault(1, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - showLog(page, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), sender, content); + int page = args.getIntOrDefault(1, 1); + if (page < 1) { + Message.LOG_INVALID_PAGE_RANGE.send(sender, 1); + return; } - } - private static void showLog(int page, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, page); + LogPage log = plugin.getStorage().getLogPage(ActionFilters.track(track), pageParams).join(); + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); + + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } - if (page == Integer.MIN_VALUE) { - page = maxPage; - } - - if (page < 1 || page > maxPage) { + if (page > maxPage) { Message.LOG_INVALID_PAGE_RANGE.send(sender, maxPage); return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); + List> entries = log.getNumberedContent(); String name = entries.stream().findAny().get().value().getTarget().getName(); Message.LOG_HISTORY_TRACK_HEADER.send(sender, name, page, maxPage); - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogUserHistory.java b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogUserHistory.java index cb04d89e9..3f5c9d960 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/log/LogUserHistory.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/log/LogUserHistory.java @@ -25,22 +25,23 @@ package me.lucko.luckperms.common.commands.log; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.util.Paginated; import me.lucko.luckperms.common.util.Predicates; import java.util.List; import java.util.UUID; -public class LogUserHistory extends ChildCommand { +public class LogUserHistory extends ChildCommand { private static final int ENTRIES_PER_PAGE = 10; public LogUserHistory() { @@ -48,39 +49,37 @@ public LogUserHistory() { } @Override - public void execute(LuckPermsPlugin plugin, Sender sender, Log log, ArgumentList args, String label) { + public void execute(LuckPermsPlugin plugin, Sender sender, Void ignored, ArgumentList args, String label) { UUID uuid = args.getUserTarget(0, plugin, sender); if (uuid == null) { return; } - Paginated content = new Paginated<>(log.getUserHistory(uuid)); - - int page = args.getIntOrDefault(1, Integer.MIN_VALUE); - if (page != Integer.MIN_VALUE) { - showLog(page, sender, content); - } else { - showLog(content.getMaxPages(ENTRIES_PER_PAGE), sender, content); + int page = args.getIntOrDefault(1, 1); + if (page < 1) { + Message.LOG_INVALID_PAGE_RANGE.send(sender, 1); + return; } - } - private static void showLog(int page, Sender sender, Paginated log) { - int maxPage = log.getMaxPages(ENTRIES_PER_PAGE); - if (maxPage == 0) { + PageParameters pageParams = new PageParameters(ENTRIES_PER_PAGE, page); + LogPage log = plugin.getStorage().getLogPage(ActionFilters.user(uuid), pageParams).join(); + int maxPage = pageParams.getMaxPage(log.getTotalEntries()); + + if (log.getTotalEntries() == 0) { Message.LOG_NO_ENTRIES.send(sender); return; } - if (page < 1 || page > maxPage) { + if (page > maxPage) { Message.LOG_INVALID_PAGE_RANGE.send(sender, maxPage); return; } - List> entries = log.getPage(page, ENTRIES_PER_PAGE); + List> entries = log.getNumberedContent(); String name = entries.stream().findAny().get().value().getTarget().getName(); Message.LOG_HISTORY_USER_HEADER.send(sender, name, page, maxPage); - for (Paginated.Entry e : entries) { + for (LogPage.Entry e : entries) { Message.LOG_ENTRY.send(sender, e.position(), e.value()); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ApplyEditsCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ApplyEditsCommand.java index dec561285..afeba3d62 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ApplyEditsCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ApplyEditsCommand.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.commands.misc; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.command.abstraction.SingleCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; @@ -47,6 +46,8 @@ public ApplyEditsCommand() { @Override public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, String label) { + boolean ignoreSessionWarning = args.remove("--force"); + String code = args.get(0); if (code.isEmpty()) { @@ -71,7 +72,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St return; } - new WebEditorResponse(data).apply(plugin, sender); + new WebEditorResponse(code, data).apply(plugin, sender, null, label, ignoreSessionWarning); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/BulkUpdateCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/BulkUpdateCommand.java index 7c5091590..b957dc6f7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/BulkUpdateCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/BulkUpdateCommand.java @@ -26,18 +26,14 @@ package me.lucko.luckperms.common.commands.misc; import com.github.benmanes.caffeine.cache.Cache; - import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.bulkupdate.BulkUpdateBuilder; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateField; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateSqlBuilder; import me.lucko.luckperms.common.bulkupdate.BulkUpdateStatistics; import me.lucko.luckperms.common.bulkupdate.DataType; import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; -import me.lucko.luckperms.common.bulkupdate.comparison.Comparison; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; -import me.lucko.luckperms.common.bulkupdate.query.Query; -import me.lucko.luckperms.common.bulkupdate.query.QueryField; import me.lucko.luckperms.common.command.abstraction.CommandException; import me.lucko.luckperms.common.command.abstraction.SingleCommand; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -45,12 +41,14 @@ import me.lucko.luckperms.common.command.utils.ArgumentException; import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.CaffeineFactory; import me.lucko.luckperms.common.util.Predicates; +import java.util.Locale; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; @@ -63,6 +61,11 @@ public BulkUpdateCommand() { @Override public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, String label) throws CommandException { + if (plugin.getConfiguration().get(ConfigKeys.DISABLE_BULKUPDATE)) { + Message.BULK_UPDATE_DISABLED.send(sender); + return; + } + if (!sender.isConsole()) { Message.BULK_UPDATE_MUST_USE_CONSOLE.send(sender); return; @@ -87,16 +90,16 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St BulkUpdateBuilder bulkUpdateBuilder = BulkUpdateBuilder.create(); - bulkUpdateBuilder.trackStatistics(!args.remove("--silent")); + bulkUpdateBuilder.trackStatistics(!args.remove("-s")); try { - bulkUpdateBuilder.dataType(DataType.valueOf(args.remove(0).toUpperCase())); + bulkUpdateBuilder.dataType(DataType.valueOf(args.remove(0).toUpperCase(Locale.ROOT))); } catch (IllegalArgumentException e) { Message.BULK_UPDATE_INVALID_DATA_TYPE.send(sender); return; } - String action = args.remove(0).toLowerCase(); + String action = args.remove(0).toLowerCase(Locale.ROOT); switch (action) { case "delete": bulkUpdateBuilder.action(DeleteAction.create()); @@ -107,7 +110,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St } String field = args.remove(0); - QueryField queryField = QueryField.of(field); + BulkUpdateField queryField = BulkUpdateField.of(field); if (queryField == null) { throw new ArgumentException.DetailedUsage(); } @@ -126,20 +129,20 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St return; } - QueryField field = QueryField.of(parts[0]); + BulkUpdateField field = BulkUpdateField.of(parts[0]); if (field == null) { Message.BULK_UPDATE_INVALID_CONSTRAINT.send(sender, constraint); return; } - Comparison comparison = StandardComparison.parseComparison(parts[1]); + Comparison comparison = Comparison.parse(parts[1]); if (comparison == null) { Message.BULK_UPDATE_INVALID_COMPARISON.send(sender, parts[1]); return; } String expr = parts[2]; - bulkUpdateBuilder.query(Query.of(field, Constraint.of(comparison, expr))); + bulkUpdateBuilder.filter(field, comparison, expr); } BulkUpdate bulkUpdate = bulkUpdateBuilder.build(); @@ -150,7 +153,11 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St String id = String.format("%04d", ThreadLocalRandom.current().nextInt(10000)); this.pendingOperations.put(id, bulkUpdate); - Message.BULK_UPDATE_QUEUED.send(sender, bulkUpdate.buildAsSql().toReadableString().replace("{table}", bulkUpdate.getDataType().getName())); + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(bulkUpdate); + String readableSql = sqlBuilder.builder().toReadableString().replace("{table}", bulkUpdate.getDataType().getName()); + + Message.BULK_UPDATE_QUEUED.send(sender, readableSql); Message.BULK_UPDATE_CONFIRM.send(sender, label, id); } } 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 3542ca1ad..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 @@ -30,7 +30,7 @@ import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.model.Track; @@ -40,11 +40,12 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.common.webeditor.WebEditorRequest; - +import me.lucko.luckperms.common.webeditor.WebEditorSession; import net.luckperms.api.node.Node; import java.util.ArrayList; import java.util.List; +import java.util.Locale; public class EditorCommand extends SingleCommand { public EditorCommand() { @@ -60,7 +61,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St String arg0 = args.getOrDefault(0, null); if (arg0 != null) { try { - type = Type.valueOf(arg0.toUpperCase()); + type = Type.valueOf(arg0.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException e) { // assume they meant it as a filter filter = arg0; @@ -106,8 +107,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St Message.EDITOR_START.send(sender); - WebEditorRequest.generate(holders, tracks, sender, label, plugin) - .createSession(plugin, sender); + WebEditorSession.create(holders, tracks, sender, label, plugin).open(); } private enum Type { diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java index f9f34f87a..3d8de5476 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ExportCommand.java @@ -38,13 +38,19 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.concurrent.atomic.AtomicBoolean; public class ExportCommand extends SingleCommand { + private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm") + .withZone(ZoneId.systemDefault()); + private final AtomicBoolean running = new AtomicBoolean(false); public ExportCommand() { - super(CommandSpec.EXPORT, "Export", CommandPermission.EXPORT, Predicates.notInRange(1, 2)); + super(CommandSpec.EXPORT, "Export", CommandPermission.EXPORT, Predicates.alwaysFalse()); } @Override @@ -68,7 +74,12 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St exporter = new Exporter.WebUpload(plugin, sender, includeUsers, includeGroups, label); } else { Path dataDirectory = plugin.getBootstrap().getDataDirectory(); - Path path = dataDirectory.resolve(args.get(0) + ".json.gz"); + Path path; + if (args.isEmpty()) { + path = dataDirectory.resolve("luckperms-" + DATE_FORMAT.format(Instant.now()) + ".json.gz"); + } else { + path = dataDirectory.resolve(args.get(0) + ".json.gz"); + } if (!path.getParent().equals(dataDirectory)) { Message.FILE_NOT_WITHIN_DIRECTORY.send(sender, path.toString()); @@ -111,4 +122,8 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St }); } + public boolean isRunning() { + return this.running.get(); + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java index 0b65e30f8..e0894120f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/ImportCommand.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.commands.misc; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.backup.Importer; import me.lucko.luckperms.common.command.abstraction.SingleCommand; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -143,4 +142,8 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St }); } + public boolean isRunning() { + return this.running.get(); + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/InfoCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/InfoCommand.java index 9acebe27e..1f4b81fa9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/InfoCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/InfoCommand.java @@ -34,10 +34,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; -import net.kyori.adventure.text.Component; - -import java.util.Map; - public class InfoCommand extends SingleCommand { public InfoCommand() { super(CommandSpec.INFO, "Info", CommandPermission.INFO, Predicates.alwaysFalse()); @@ -45,8 +41,7 @@ public InfoCommand() { @Override public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, String label) { - Map storageMeta = plugin.getStorage().getMeta(); - Message.INFO.send(sender, plugin, storageMeta); + Message.INFO.send(sender, plugin, plugin.getStorage().getMeta()); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/SearchCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/SearchCommand.java index 49c3048da..958ebbb07 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/SearchCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/SearchCommand.java @@ -26,10 +26,6 @@ package me.lucko.luckperms.common.commands.misc; import com.google.common.collect.Maps; - -import me.lucko.luckperms.common.bulkupdate.comparison.Comparison; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; import me.lucko.luckperms.common.cache.LoadingMap; import me.lucko.luckperms.common.command.abstraction.SingleCommand; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -37,6 +33,7 @@ import me.lucko.luckperms.common.command.tabcomplete.TabCompleter; import me.lucko.luckperms.common.command.tabcomplete.TabCompletions; import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.node.comparator.NodeEntryComparator; @@ -47,7 +44,6 @@ import me.lucko.luckperms.common.storage.misc.NodeEntry; import me.lucko.luckperms.common.util.Iterators; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.node.Node; import java.util.ArrayList; @@ -64,13 +60,13 @@ public SearchCommand() { @Override public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, String label) { - Comparison comparison = StandardComparison.parseComparison(args.get(0)); + Comparison comparison = Comparison.parse(args.get(0)); if (comparison == null) { - comparison = StandardComparison.EQUAL; + comparison = Comparison.EQUAL; args.add(0, "=="); } - ConstraintNodeMatcher matcher = StandardNodeMatchers.of(Constraint.of(comparison, args.get(1))); + ConstraintNodeMatcher matcher = StandardNodeMatchers.key(args.get(1), comparison); int page = args.getIntOrDefault(2, 1); Message.SEARCH_SEARCHING.send(sender, matcher.toString()); @@ -102,7 +98,7 @@ public List tabComplete(LuckPermsPlugin plugin, Sender sender, ArgumentL private static > void sendResult(Sender sender, List> results, Function lookupFunction, Message.Args3 headerMessage, HolderType holderType, String label, int page, Comparison comparison) { results = new ArrayList<>(results); - results.sort(NodeEntryComparator.normal()); + results.sort(NodeEntryComparator.ascending()); int pageIndex = page - 1; List>> pages = Iterators.divideIterable(results, 15); @@ -122,7 +118,7 @@ private static > void sendResult(Sender sender, List> ent : mappedContent) { - Message.SEARCH_NODE_ENTRY.send(sender, comparison != StandardComparison.EQUAL, ent.getValue().getNode(), ent.getKey(), holderType, label, sender.getPlugin()); + Message.SEARCH_NODE_ENTRY.send(sender, comparison != Comparison.EQUAL, ent.getValue().getNode(), ent.getKey(), holderType, label, sender.getPlugin()); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/TranslationsCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/TranslationsCommand.java index 76061e9d4..81f810552 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/TranslationsCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/TranslationsCommand.java @@ -36,10 +36,10 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.kyori.adventure.text.Component; import java.io.IOException; +import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; @@ -70,12 +70,12 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St return; } - Message.INSTALLED_TRANSLATIONS.send(sender, plugin.getTranslationManager().getInstalledLocales().stream().map(Locale::toString).collect(Collectors.toList())); + Message.INSTALLED_TRANSLATIONS.send(sender, plugin.getTranslationManager().getInstalledLocales().stream().map(Locale::toLanguageTag).sorted().collect(Collectors.toList())); Message.AVAILABLE_TRANSLATIONS_HEADER.send(sender); - for (LanguageInfo language : availableTranslations) { - Message.AVAILABLE_TRANSLATIONS_ENTRY.send(sender, language.locale().toString(), TranslationManager.localeDisplayName(language.locale()), language.progress(), language.contributors()); - } + availableTranslations.stream() + .sorted(Comparator.comparing(language -> language.locale().toLanguageTag())) + .forEach(language -> Message.AVAILABLE_TRANSLATIONS_ENTRY.send(sender, language.locale().toLanguageTag(), TranslationManager.localeDisplayName(language.locale()), language.progress(), language.contributors())); sender.sendMessage(Message.prefixed(Component.empty())); Message.TRANSLATIONS_DOWNLOAD_PROMPT.send(sender, label); } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/TrustEditorCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/TrustEditorCommand.java new file mode 100644 index 000000000..417a0d589 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/TrustEditorCommand.java @@ -0,0 +1,69 @@ +/* + * 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.commands.misc; + +import me.lucko.luckperms.common.command.abstraction.SingleCommand; +import me.lucko.luckperms.common.command.access.CommandPermission; +import me.lucko.luckperms.common.command.spec.CommandSpec; +import me.lucko.luckperms.common.command.utils.ArgumentList; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.Predicates; +import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; + +public class TrustEditorCommand extends SingleCommand { + public TrustEditorCommand() { + super(CommandSpec.TRUST_EDITOR, "TrustEditor", CommandPermission.TRUST_EDITOR, Predicates.not(1)); + } + + @Override + public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, String label) { + String id = args.get(0); + + if (id.isEmpty()) { + Message.APPLY_EDITS_INVALID_CODE.send(sender, id); + return; + } + + WebEditorSocket socket = plugin.getWebEditorStore().sockets().getSocket(sender); + if (socket == null) { + Message.EDITOR_SOCKET_TRUST_FAILURE.send(sender); + return; + } + + if (socket.trustConnection(id)) { + Message.EDITOR_SOCKET_TRUST_SUCCESS.send(sender); + } else { + Message.EDITOR_SOCKET_TRUST_FAILURE.send(sender); + } + } + + @Override + public boolean shouldDisplay() { + return false; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/VerboseCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/VerboseCommand.java index a9acd5361..4b80dfb66 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/VerboseCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/VerboseCommand.java @@ -45,6 +45,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; public class VerboseCommand extends SingleCommand { public VerboseCommand() { @@ -59,7 +60,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St } VerboseHandler verboseHandler = plugin.getVerboseHandler(); - String mode = args.get(0).toLowerCase(); + String mode = args.get(0).toLowerCase(Locale.ROOT); if (mode.equals("command") || mode.equals("cmd")) { if (args.size() < 3) { @@ -93,7 +94,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St String commandWithSlash = String.join(" ", args.subList(2, args.size())); String command = commandWithSlash.charAt(0) == '/' ? commandWithSlash.substring(1) : commandWithSlash; - plugin.getBootstrap().getScheduler().sync().execute(() -> { + plugin.getBootstrap().getScheduler().executeSync(executor, () -> { Message.VERBOSE_ON_COMMAND.send(sender, executor.getName(), command); verboseHandler.registerListener(sender, VerboseFilter.acceptAll(), true); @@ -185,7 +186,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St @Override public List tabComplete(LuckPermsPlugin plugin, Sender sender, ArgumentList args) { return TabCompleter.create() - .at(0, CompletionSupplier.startsWith("on", "record", "off", "upload", "command")) + .at(0, CompletionSupplier.startsWith("on", "record", "off", "upload", "paste", "command")) .complete(args); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/CreateTrack.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/CreateTrack.java index 7f0e76380..0a11aba63 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/CreateTrack.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/CreateTrack.java @@ -35,11 +35,12 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.kyori.adventure.text.Component; import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.cause.CreationCause; +import java.util.Locale; + public class CreateTrack extends SingleCommand { public CreateTrack() { super(CommandSpec.CREATE_TRACK, "CreateTrack", CommandPermission.CREATE_TRACK, Predicates.not(1)); @@ -52,7 +53,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St return; } - String trackName = args.get(0).toLowerCase(); + String trackName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.TRACK_NAME_TEST.test(trackName)) { Message.TRACK_INVALID_ENTRY.send(sender, trackName); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/DeleteTrack.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/DeleteTrack.java index 75c250a25..3b1e3a8f8 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/DeleteTrack.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/DeleteTrack.java @@ -38,12 +38,12 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.kyori.adventure.text.Component; import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.cause.DeletionCause; import java.util.List; +import java.util.Locale; public class DeleteTrack extends SingleCommand { public DeleteTrack() { @@ -57,7 +57,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, ArgumentList args, St return; } - String trackName = args.get(0).toLowerCase(); + String trackName = args.get(0).toLowerCase(Locale.ROOT); Track track = plugin.getStorage().loadTrack(trackName).join().orElse(null); if (track == null) { Message.TRACK_LOAD_ERROR.send(sender); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackAppend.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackAppend.java index 898dfc4b0..d87176e95 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackAppend.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackAppend.java @@ -41,10 +41,10 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.model.data.DataMutateResult; import java.util.List; +import java.util.Locale; public class TrackAppend extends ChildCommand { public TrackAppend() { @@ -58,7 +58,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Track target, Argumen return; } - String groupName = args.get(0).toLowerCase(); + String groupName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.GROUP_NAME_TEST.test(groupName)) { sendDetailedUsage(sender, label); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackClone.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackClone.java index 8abddced4..b52a54e12 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackClone.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackClone.java @@ -38,10 +38,11 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.kyori.adventure.text.Component; import net.luckperms.api.event.cause.CreationCause; +import java.util.Locale; + public class TrackClone extends ChildCommand { public TrackClone() { super(CommandSpec.TRACK_CLONE, "clone", CommandPermission.TRACK_CLONE, Predicates.not(1)); @@ -54,7 +55,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Track target, Argumen return; } - String newTrackName = args.get(0).toLowerCase(); + String newTrackName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.TRACK_NAME_TEST.test(newTrackName)) { Message.TRACK_INVALID_ENTRY.send(sender, newTrackName); return; 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 77404b6d7..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 @@ -30,7 +30,7 @@ import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; import me.lucko.luckperms.common.command.utils.ArgumentList; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.PermissionHolder; @@ -42,7 +42,7 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.common.webeditor.WebEditorRequest; - +import me.lucko.luckperms.common.webeditor.WebEditorSession; import net.luckperms.api.node.Node; import java.util.ArrayList; @@ -97,8 +97,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Track target, Argumen Message.EDITOR_START.send(sender); - WebEditorRequest.generate(holders, Collections.singletonList(target), sender, label, plugin) - .createSession(plugin, sender); + WebEditorSession.create(holders, Collections.singletonList(target), sender, label, plugin).open(); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackInsert.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackInsert.java index 3c77671f4..44461877d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackInsert.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackInsert.java @@ -41,10 +41,10 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.model.data.DataMutateResult; import java.util.List; +import java.util.Locale; public class TrackInsert extends ChildCommand { public TrackInsert() { @@ -58,7 +58,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Track target, Argumen return; } - String groupName = args.get(0).toLowerCase(); + String groupName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.GROUP_NAME_TEST.test(groupName)) { sendDetailedUsage(sender, label); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackParentCommand.java index 770efc4d2..505747ce7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackParentCommand.java @@ -27,7 +27,6 @@ import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.command.abstraction.Command; import me.lucko.luckperms.common.command.abstraction.ParentCommand; import me.lucko.luckperms.common.command.spec.CommandSpec; @@ -39,6 +38,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -53,7 +53,7 @@ public class TrackParentCommand extends ParentCommand { .build(key -> new ReentrantLock()); public TrackParentCommand() { - super(CommandSpec.TRACK, "Track", Type.TAKES_ARGUMENT_FOR_TARGET, ImmutableList.>builder() + super(CommandSpec.TRACK, "Track", Type.TARGETED, ImmutableList.>builder() .add(new TrackInfo()) .add(new TrackEditor()) .add(new TrackAppend()) @@ -68,7 +68,7 @@ public TrackParentCommand() { @Override protected String parseTarget(String target, LuckPermsPlugin plugin, Sender sender) { - return target.toLowerCase(); + return target.toLowerCase(Locale.ROOT); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackRemove.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackRemove.java index cd2464e52..b9218729f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackRemove.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackRemove.java @@ -40,10 +40,10 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.model.data.DataMutateResult; import java.util.List; +import java.util.Locale; public class TrackRemove extends ChildCommand { public TrackRemove() { @@ -57,7 +57,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Track target, Argumen return; } - String groupName = args.get(0).toLowerCase(); + String groupName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.GROUP_NAME_TEST.test(groupName)) { sendDetailedUsage(sender, label); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackRename.java b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackRename.java index c88c8a879..8a4929c12 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackRename.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/track/TrackRename.java @@ -38,11 +38,12 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.kyori.adventure.text.Component; import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.event.cause.DeletionCause; +import java.util.Locale; + public class TrackRename extends ChildCommand { public TrackRename() { super(CommandSpec.TRACK_RENAME, "rename", CommandPermission.TRACK_RENAME, Predicates.not(1)); @@ -55,7 +56,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Track target, Argumen return; } - String newTrackName = args.get(0).toLowerCase(); + String newTrackName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.TRACK_NAME_TEST.test(newTrackName)) { Message.TRACK_INVALID_ENTRY.send(sender, newTrackName); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserClone.java b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserClone.java index 6769e9550..a7161c740 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserClone.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserClone.java @@ -37,7 +37,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.model.data.DataType; import java.util.UUID; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserDemote.java b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserDemote.java index 0dd89f5e5..d46879cb6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserDemote.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserDemote.java @@ -42,11 +42,11 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.track.DemotionResult; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.function.Predicate; @@ -77,7 +77,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, User target, Argument } } - final String trackName = args.get(0).toLowerCase(); + final String trackName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.TRACK_NAME_TEST.test(trackName)) { Message.TRACK_INVALID_ENTRY.send(sender, trackName); return; diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserInfo.java b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserInfo.java index 41a8cc7d2..67f9f68bd 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserInfo.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserInfo.java @@ -25,7 +25,7 @@ package me.lucko.luckperms.common.commands.user; -import me.lucko.luckperms.common.cacheddata.type.MetaCache; +import me.lucko.luckperms.common.cacheddata.type.MonitoredMetaCache; import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -37,13 +37,13 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.common.util.UniqueIdType; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.context.ContextSet; import net.luckperms.api.node.Node; import net.luckperms.api.node.types.InheritanceNode; import net.luckperms.api.query.QueryOptions; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -67,26 +67,23 @@ public void execute(LuckPermsPlugin plugin, Sender sender, User target, Argument plugin.getBootstrap().isPlayerOnline(target.getUniqueId()) ); - List parents = target.normalData().inheritanceAsSortedSet().stream() + Map> parents = target.normalData().inheritanceAsSortedSet().stream() .filter(Node::getValue) - .filter(n -> !n.hasExpiry()) - .collect(Collectors.toList()); + .collect(Collectors.groupingBy(Node::hasExpiry, Collectors.toList())); - List tempParents = target.normalData().inheritanceAsSortedSet().stream() - .filter(Node::getValue) - .filter(Node::hasExpiry) - .collect(Collectors.toList()); + List temporaryParents = parents.getOrDefault(true, Collections.emptyList()); + List permanentParents = parents.getOrDefault(false, Collections.emptyList()); - if (!parents.isEmpty()) { + if (!permanentParents.isEmpty()) { Message.INFO_PARENT_HEADER.send(sender); - for (InheritanceNode node : parents) { + for (InheritanceNode node : permanentParents) { Message.INFO_PARENT_NODE_ENTRY.send(sender, node); } } - if (!tempParents.isEmpty()) { + if (!temporaryParents.isEmpty()) { Message.INFO_TEMP_PARENT_HEADER.send(sender); - for (InheritanceNode node : tempParents) { + for (InheritanceNode node : temporaryParents) { Message.INFO_PARENT_TEMPORARY_NODE_ENTRY.send(sender, node); } } @@ -100,11 +97,11 @@ public void execute(LuckPermsPlugin plugin, Sender sender, User target, Argument } ContextSet contextSet = queryOptions.context(); - MetaCache data = target.getCachedData().getMetaData(queryOptions); - String prefix = data.getPrefix(MetaCheckEvent.Origin.INTERNAL); - String suffix = data.getSuffix(MetaCheckEvent.Origin.INTERNAL); - String primaryGroup = data.getPrimaryGroup(MetaCheckEvent.Origin.INTERNAL); - Map> meta = data.getMeta(MetaCheckEvent.Origin.INTERNAL); + MonitoredMetaCache data = target.getCachedData().getMetaData(queryOptions); + String prefix = data.getPrefix(CheckOrigin.INTERNAL).result(); + String suffix = data.getSuffix(CheckOrigin.INTERNAL).result(); + String primaryGroup = data.getPrimaryGroup(CheckOrigin.INTERNAL); + Map> meta = data.getMeta(CheckOrigin.INTERNAL); Message.USER_INFO_CONTEXTUAL_DATA.send(sender, active, contextSet, prefix, suffix, primaryGroup, meta); } diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserParentCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserParentCommand.java index 64d2ce346..eafa37a67 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserParentCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserParentCommand.java @@ -27,7 +27,6 @@ import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.command.abstraction.Command; import me.lucko.luckperms.common.command.abstraction.ParentCommand; import me.lucko.luckperms.common.command.spec.CommandSpec; @@ -63,7 +62,7 @@ public class UserParentCommand extends ParentCommand { .build(key -> new ReentrantLock()); public UserParentCommand() { - super(CommandSpec.USER, "User", Type.TAKES_ARGUMENT_FOR_TARGET, ImmutableList.>builder() + super(CommandSpec.USER, "User", Type.TARGETED, ImmutableList.>builder() .add(new UserInfo()) .add(new CommandPermission<>(HolderType.USER)) .add(new CommandParent<>(HolderType.USER)) diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserPromote.java b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserPromote.java index c7faa2b11..36c6b2cad 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/user/UserPromote.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/user/UserPromote.java @@ -42,11 +42,11 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.DataConstraints; import me.lucko.luckperms.common.util.Predicates; - import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.track.PromotionResult; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.function.Predicate; @@ -77,7 +77,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, User target, Argument } } - final String trackName = args.get(0).toLowerCase(); + final String trackName = args.get(0).toLowerCase(Locale.ROOT); if (!DataConstraints.TRACK_NAME_TEST.test(trackName)) { Message.TRACK_INVALID_ENTRY.send(sender, trackName); return; 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 1c4a27204..c7d1d9b77 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 @@ -28,15 +28,14 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; - +import me.lucko.luckperms.common.cacheddata.metastack.SimpleMetaStackDefinition; +import me.lucko.luckperms.common.cacheddata.metastack.StandardStackElements; import me.lucko.luckperms.common.cacheddata.type.SimpleMetaValueSelector; import me.lucko.luckperms.common.config.generic.KeyedConfiguration; import me.lucko.luckperms.common.config.generic.key.ConfigKey; import me.lucko.luckperms.common.config.generic.key.SimpleConfigKey; -import me.lucko.luckperms.common.context.WorldNameRewriter; +import me.lucko.luckperms.common.context.calculator.WorldNameRewriter; import me.lucko.luckperms.common.graph.TraversalAlgorithm; -import me.lucko.luckperms.common.metastacking.SimpleMetaStackDefinition; -import me.lucko.luckperms.common.metastacking.StandardStackElements; import me.lucko.luckperms.common.model.PrimaryGroupHolder; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.query.QueryOptionsBuilderImpl; @@ -44,7 +43,7 @@ import me.lucko.luckperms.common.storage.implementation.split.SplitStorageType; import me.lucko.luckperms.common.storage.misc.StorageCredentials; import me.lucko.luckperms.common.util.ImmutableCollectors; - +import me.lucko.luckperms.common.util.Predicates; import net.luckperms.api.context.ContextSatisfyMode; import net.luckperms.api.metastacking.DuplicateRemovalFunction; import net.luckperms.api.metastacking.MetaStackDefinition; @@ -59,10 +58,12 @@ import java.util.EnumMap; import java.util.EnumSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; +import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -72,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. @@ -85,13 +87,7 @@ private ConfigKeys() {} /** * The name of the server */ - public static final ConfigKey SERVER = key(c -> { - String server = c.getString("server", "global").toLowerCase(); - if (server.equals("load-from-system-property")) { - server = System.getProperty("luckperms.server", "global").toLowerCase(); - } - return server; - }); + public static final ConfigKey SERVER = lowercaseStringKey("server", "global"); /** * How many minutes to wait between syncs. A value <= 0 will disable syncing. @@ -161,6 +157,11 @@ private ConfigKeys() {} */ public static final ConfigKey SKIP_BULKUPDATE_CONFIRMATION = booleanKey("skip-bulkupdate-confirmation", false); + /** + * If LuckPerms should prevent bulkupdate operations. + */ + public static final ConfigKey DISABLE_BULKUPDATE = booleanKey("disable-bulkupdate", false); + /** * If LuckPerms should produce extra logging output when it handles logins. */ @@ -191,7 +192,7 @@ private ConfigKeys() {} */ public static final ConfigKey TEMPORARY_ADD_BEHAVIOUR = key(c -> { String value = c.getString("temporary-add-behaviour", "deny"); - switch (value.toLowerCase()) { + switch (value.toLowerCase(Locale.ROOT)) { case "accumulate": return TemporaryNodeMergeStrategy.ADD_NEW_DURATION_TO_EXISTING; case "replace": @@ -205,7 +206,7 @@ private ConfigKeys() {} * How primary groups should be calculated. */ public static final ConfigKey PRIMARY_GROUP_CALCULATION_METHOD = notReloadable(key(c -> { - String option = c.getString("primary-group-calculation", "stored").toLowerCase(); + String option = c.getString("primary-group-calculation", "stored").toLowerCase(Locale.ROOT); if (!option.equals("stored") && !option.equals("parents-by-weight") && !option.equals("all-parents-by-weight")) { option = "stored"; } @@ -245,6 +246,26 @@ private ConfigKeys() {} */ public static final ConfigKey REQUIRE_SENDER_GROUP_MEMBERSHIP_TO_MODIFY = booleanKey("require-sender-group-membership-to-modify", false); + /** + * If the plugin is in read-only mode for players + */ + public static final ConfigKey READ_ONLY_MODE_PLAYERS = booleanKey("commands-read-only-mode.players", false); + + /** + * If the plugin is in read-only mode for the server console + */ + public static final ConfigKey READ_ONLY_MODE_CONSOLE = booleanKey("commands-read-only-mode.console", false); + + /** + * If LuckPerms commands are disabled for players + */ + public static final ConfigKey DISABLE_LUCKPERMS_COMMANDS_PLAYERS = booleanKey("disable-luckperms-commands.players", false); + + /** + * If LuckPerms commands are disabled for the server console + */ + public static final ConfigKey DISABLE_LUCKPERMS_COMMANDS_CONSOLE = booleanKey("disable-luckperms-commands.console", false); + /** * If wildcards are being applied */ @@ -313,12 +334,22 @@ private ConfigKeys() {} */ public static final ConfigKey APPLY_SPONGE_DEFAULT_SUBJECTS = notReloadable(booleanKey("apply-sponge-default-subjects", true)); + /** + * If Hytale virtual groups should be applied + */ + public static final ConfigKey APPLY_HYTALE_VIRTUAL_GROUPS = notReloadable(booleanKey("apply-hytale-virtual-groups", true)); + + /** + * If LuckPerms should delegate permission checks to the Hytale permissions provider for users/groups not managed by LuckPerms + */ + public static final ConfigKey DELEGATE_TO_HYTALE_PERMISSIONS_PROVIDER = notReloadable(booleanKey("delegate-to-hytale-permissions-provider", true)); + /** * The algorithm LuckPerms should use when traversing the "inheritance tree" */ public static final ConfigKey INHERITANCE_TRAVERSAL_ALGORITHM = key(c -> { String value = c.getString("inheritance-traversal-algorithm", "depth-first-pre-order"); - switch (value.toLowerCase()) { + switch (value.toLowerCase(Locale.ROOT)) { case "breadth-first": return TraversalAlgorithm.BREADTH_FIRST; case "depth-first-post-order": @@ -355,7 +386,7 @@ private ConfigKeys() {} */ public static final ConfigKey> GROUP_WEIGHTS = key(c -> { return c.getStringMap("group-weight", ImmutableMap.of()).entrySet().stream().collect(ImmutableCollectors.toMap( - e -> e.getKey().toLowerCase(), + e -> e.getKey().toLowerCase(Locale.ROOT), e -> { try { return Integer.parseInt(e.getValue()); @@ -369,16 +400,16 @@ private ConfigKeys() {} /** * Creates a new prefix MetaStack element based upon the configured values. */ - public static final ConfigKey PREFIX_FORMATTING_OPTIONS = key(l -> { - List format = l.getStringList("meta-formatting.prefix.format", new ArrayList<>()); + public static final ConfigKey PREFIX_FORMATTING_OPTIONS = key(c -> { + List format = c.getStringList("meta-formatting.prefix.format", new ArrayList<>()); if (format.isEmpty()) { format.add("highest"); } - String startSpacer = l.getString("meta-formatting.prefix.start-spacer", ""); - String middleSpacer = l.getString("meta-formatting.prefix.middle-spacer", " "); - String endSpacer = l.getString("meta-formatting.prefix.end-spacer", ""); + String startSpacer = c.getString("meta-formatting.prefix.start-spacer", ""); + String middleSpacer = c.getString("meta-formatting.prefix.middle-spacer", " "); + String endSpacer = c.getString("meta-formatting.prefix.end-spacer", ""); DuplicateRemovalFunction duplicateRemovalFunction; - switch (l.getString("meta-formatting.prefix.duplicates", "").toLowerCase()) { + switch (c.getString("meta-formatting.prefix.duplicates", "").toLowerCase(Locale.ROOT)) { case "first-only": duplicateRemovalFunction = DuplicateRemovalFunction.FIRST_ONLY; break; @@ -390,22 +421,22 @@ private ConfigKeys() {} break; } - return new SimpleMetaStackDefinition(StandardStackElements.parseList(l.getPlugin(), format), duplicateRemovalFunction, startSpacer, middleSpacer, endSpacer); + return new SimpleMetaStackDefinition(StandardStackElements.parseList(c.getPlugin(), format), duplicateRemovalFunction, startSpacer, middleSpacer, endSpacer); }); /** * Creates a new suffix MetaStack element based upon the configured values. */ - public static final ConfigKey SUFFIX_FORMATTING_OPTIONS = key(l -> { - List format = l.getStringList("meta-formatting.suffix.format", new ArrayList<>()); + public static final ConfigKey SUFFIX_FORMATTING_OPTIONS = key(c -> { + List format = c.getStringList("meta-formatting.suffix.format", new ArrayList<>()); if (format.isEmpty()) { format.add("highest"); } - String startSpacer = l.getString("meta-formatting.suffix.start-spacer", ""); - String middleSpacer = l.getString("meta-formatting.suffix.middle-spacer", " "); - String endSpacer = l.getString("meta-formatting.suffix.end-spacer", ""); + String startSpacer = c.getString("meta-formatting.suffix.start-spacer", ""); + String middleSpacer = c.getString("meta-formatting.suffix.middle-spacer", " "); + String endSpacer = c.getString("meta-formatting.suffix.end-spacer", ""); DuplicateRemovalFunction duplicateRemovalFunction; - switch (l.getString("meta-formatting.prefix.duplicates", "").toLowerCase()) { + switch (c.getString("meta-formatting.suffix.duplicates", "").toLowerCase(Locale.ROOT)) { case "first-only": duplicateRemovalFunction = DuplicateRemovalFunction.FIRST_ONLY; break; @@ -417,7 +448,7 @@ private ConfigKeys() {} break; } - return new SimpleMetaStackDefinition(StandardStackElements.parseList(l.getPlugin(), format), duplicateRemovalFunction, startSpacer, middleSpacer, endSpacer); + return new SimpleMetaStackDefinition(StandardStackElements.parseList(c.getPlugin(), format), duplicateRemovalFunction, startSpacer, middleSpacer, endSpacer); }); /** @@ -442,6 +473,11 @@ private ConfigKeys() {} .collect(ImmutableCollectors.toList()); }); + /** + * If log should be posted synchronously to storage/messaging in commands + */ + public static final ConfigKey LOG_SYNCHRONOUSLY_IN_COMMANDS = booleanKey("log-synchronously-in-commands", false); + /** * If LuckPerms should automatically install translation bundles and periodically update them. */ @@ -462,6 +498,11 @@ private ConfigKeys() {} */ public static final ConfigKey COMMANDS_ALLOW_OP = notReloadable(booleanKey("commands-allow-op", true)); + /** + * If LuckPerms should rate-limit command executions. + */ + public static final ConfigKey COMMANDS_RATE_LIMIT = booleanKey("commands-rate-limit", true); + /** * If Vault lookups for offline players on the main server thread should be enabled */ @@ -490,14 +531,7 @@ private ConfigKeys() {} /** * The name of the server to use for Vault. */ - public static final ConfigKey VAULT_SERVER = key(c -> { - // default to true for backwards compatibility - if (USE_VAULT_SERVER.get(c)) { - return c.getString("vault-server", "global").toLowerCase(); - } else { - return SERVER.get(c); - } - }); + public static final ConfigKey VAULT_SERVER = lowercaseStringKey("vault-server", "global"); /** * If Vault should apply global permissions @@ -510,9 +544,19 @@ private ConfigKeys() {} public static final ConfigKey VAULT_IGNORE_WORLD = booleanKey("vault-ignore-world", false); /** - * If the owner of an integrated server should automatically bypasses all permission checks. On fabric, this only applies on an Integrated Server. + * If the owner of an integrated server should automatically bypass all permission checks. On fabric and forge, this only applies on an Integrated Server. + */ + public static final ConfigKey INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS = booleanKey("integrated-server-owner-bypasses-checks", true); + + /** + * Disabled context calculators */ - public static final ConfigKey FABRIC_INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS = booleanKey("integrated-server-owner-bypasses-checks", true); + public static final ConfigKey>> DISABLED_CONTEXT_CALCULATORS = key(c -> { + return c.getStringList("disabled-context-calculators", ImmutableList.of()) + .stream() + .map(Predicates::startsWithIgnoreCase) + .collect(ImmutableCollectors.toSet()); + }); /** * The world rewrites map @@ -520,8 +564,8 @@ private ConfigKeys() {} public static final ConfigKey WORLD_REWRITES = key(c -> { return WorldNameRewriter.of(c.getStringMap("world-rewrite", ImmutableMap.of()).entrySet().stream() .collect(ImmutableCollectors.toMap( - e -> e.getKey().toLowerCase(), - e -> e.getValue().toLowerCase() + e -> e.getKey().toLowerCase(Locale.ROOT), + e -> e.getValue().toLowerCase(Locale.ROOT) ))); }); @@ -571,6 +615,16 @@ private ConfigKeys() {} return c.getString("data.mongodb-connection-uri", c.getString("data.mongodb_connection_URI", "")); })); + /** + * The REST storage URL + */ + public static final ConfigKey REST_STORAGE_URL = notReloadable(stringKey("data.rest-url", "http://localhost:8080/")); + + /** + * The REST storage auth key + */ + public static final ConfigKey REST_STORAGE_AUTH_KEY = notReloadable(stringKey("data.rest-auth-key", "")); + /** * The name of the storage method being used */ @@ -631,6 +685,16 @@ 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 + */ + public static final ConfigKey REDIS_USERNAME = notReloadable(stringKey("redis.username", "")); + /** * The password in use by the redis server, or an empty string if there is no password */ @@ -641,6 +705,61 @@ private ConfigKeys() {} */ public static final ConfigKey REDIS_SSL = notReloadable(booleanKey("redis.ssl", false)); + /** + * If redis sentinel is enabled + */ + public static final ConfigKey REDIS_SENTINEL_ENABLED = notReloadable(booleanKey("redis.sentinel.enabled", false)); + + /** + * The name of the redis sentinel master + */ + public static final ConfigKey REDIS_SENTINEL_MASTER = notReloadable(stringKey("redis.sentinel.master", "mymaster")); + + /** + * The addresses of the redis sentinel nodes + */ + public static final ConfigKey> REDIS_SENTINEL_ADDRESSES = notReloadable(stringListKey("redis.sentinel.addresses", ImmutableList.of())); + + /** + * The username to connect to the redis sentinel nodes with, or an empty string if it should use default + */ + public static final ConfigKey REDIS_SENTINEL_USERNAME = notReloadable(stringKey("redis.sentinel.username", "")); + + /** + * The password in use by the redis sentinel nodes, or an empty string if there is no password + */ + public static final ConfigKey REDIS_SENTINEL_PASSWORD = notReloadable(stringKey("redis.sentinel.password", "")); + + /** + * If nats messaging is enabled + */ + public static final ConfigKey NATS_ENABLED = notReloadable(booleanKey("nats.enabled", false)); + + /** + * The address of the nats server + */ + public static final ConfigKey NATS_ADDRESS = notReloadable(stringKey("nats.address", null)); + + /** + * The username to connect with, or an empty string if it should use default + */ + public static final ConfigKey NATS_USERNAME = notReloadable(stringKey("nats.username", "")); + + /** + * The password in use by the nats server, or an empty string if there is no password + */ + public static final ConfigKey NATS_PASSWORD = notReloadable(stringKey("nats.password", "")); + + /** + * The token in use by the nats server, or an empty string if there is no token + */ + public static final ConfigKey NATS_TOKEN = notReloadable(stringKey("nats.token", "")); + + /** + * If the nats connection should use SSL + */ + public static final ConfigKey NATS_SSL = notReloadable(booleanKey("nats.ssl", false)); + /** * If rabbitmq messaging is enabled */ @@ -666,10 +785,25 @@ private ConfigKeys() {} */ public static final ConfigKey RABBITMQ_PASSWORD = notReloadable(stringKey("rabbitmq.password", "guest")); + /** + * If the chat formatter is enabled - deprecated + */ + public static final ConfigKey CHAT_FORMATTER_ENABLED = booleanKey("chat-formatter.enabled", false); + + /** + * If the editor key should be generated lazily (only when needed) + */ + public static final ConfigKey EDITOR_LAZILY_GENERATE_KEY = booleanKey("editor-lazily-generate-key", false); + /** * The URL of the bytebin instance used to upload data */ - public static final ConfigKey BYTEBIN_URL = stringKey("bytebin-url", "https://bytebin.lucko.me/"); + public static final ConfigKey BYTEBIN_URL = notReloadable(stringKey("bytebin-url", "https://usercontent.luckperms.net/")); + + /** + * The URL of the bytesocks instance used to communicate with + */ + public static final ConfigKey BYTESOCKS_URL = notReloadable(stringKey("bytesocks-url", "https://usersockets.luckperms.net/")); /** * The URL of the web editor @@ -696,4 +830,15 @@ public static List> getKeys() { return KEYS; } + /** + * Check if the value at the given path should be censored in console/log output + * + * @param path the path + * @return true if the value should be censored + */ + public static boolean shouldCensorValue(final String path) { + final String lower = path.toLowerCase(Locale.ROOT); + return lower.contains("password") || lower.contains("uri"); + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ContextsFile.java b/common/src/main/java/me/lucko/luckperms/common/config/ContextsFile.java index 851ca18e0..593038d13 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/ContextsFile.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/ContextsFile.java @@ -27,11 +27,9 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; - -import me.lucko.luckperms.common.context.ContextSetJsonSerializer; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.serializer.ContextSetJsonSerializer; import me.lucko.luckperms.common.util.gson.GsonProvider; - import net.luckperms.api.context.ImmutableContextSet; import java.io.BufferedReader; diff --git a/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/ConfigurateConfigAdapter.java b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/ConfigurateConfigAdapter.java index 3a02cfce3..d37eefe30 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/ConfigurateConfigAdapter.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/ConfigurateConfigAdapter.java @@ -26,9 +26,7 @@ package me.lucko.luckperms.common.config.generic.adapter; import com.google.common.base.Splitter; - import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.loader.ConfigurationLoader; @@ -95,16 +93,6 @@ public List getStringList(String path, List def) { return node.getList(Object::toString); } - @Override - public List getKeys(String path, List def) { - ConfigurationNode node = resolvePath(path); - if (node.isVirtual() || !node.isMap()) { - return def; - } - - return node.getChildrenMap().keySet().stream().map(Object::toString).collect(Collectors.toList()); - } - @SuppressWarnings("unchecked") @Override public Map getStringMap(String path, Map def) { diff --git a/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/ConfigurationAdapter.java b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/ConfigurationAdapter.java index d91d67eab..e732845c7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/ConfigurationAdapter.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/ConfigurationAdapter.java @@ -44,8 +44,6 @@ public interface ConfigurationAdapter { List getStringList(String path, List def); - List getKeys(String path, List def); - Map getStringMap(String path, Map def); } diff --git a/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/EnvironmentVariableConfigAdapter.java b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/EnvironmentVariableConfigAdapter.java new file mode 100644 index 000000000..d9f7445c0 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/EnvironmentVariableConfigAdapter.java @@ -0,0 +1,69 @@ +/* + * 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.config.generic.adapter; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Locale; + +public class EnvironmentVariableConfigAdapter extends StringBasedConfigurationAdapter { + private static final String PREFIX = "LUCKPERMS_"; + + private final LuckPermsPlugin plugin; + + public EnvironmentVariableConfigAdapter(LuckPermsPlugin plugin) { + this.plugin = plugin; + } + + @Override + protected @Nullable String resolveValue(String path) { + // e.g. + // 'server' -> LUCKPERMS_SERVER + // 'data.table_prefix' -> LUCKPERMS_DATA_TABLE_PREFIX + String key = PREFIX + path.toUpperCase(Locale.ROOT) + .replace('-', '_') + .replace('.', '_'); + + String value = System.getenv(key); + if (value != null) { + String printableValue = ConfigKeys.shouldCensorValue(path) ? "*****" : value; + this.plugin.getLogger().info(String.format("Resolved configuration value from environment variable: %s = %s", key, printableValue)); + } + return value; + } + + @Override + public LuckPermsPlugin getPlugin() { + return this.plugin; + } + + @Override + public void reload() { + // no-op + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/FileSecretConfigAdapter.java b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/FileSecretConfigAdapter.java new file mode 100644 index 000000000..b73fde0b9 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/FileSecretConfigAdapter.java @@ -0,0 +1,90 @@ +/* + * 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.config.generic.adapter; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; + +public class FileSecretConfigAdapter extends StringBasedConfigurationAdapter { + private static final String PREFIX = "luckperms_"; + + private final LuckPermsPlugin plugin; + private final Path directory; + + public FileSecretConfigAdapter(LuckPermsPlugin plugin, String directory) { + this.plugin = plugin; + this.directory = directory == null ? null : Paths.get(directory); + } + + public FileSecretConfigAdapter(LuckPermsPlugin plugin) { + this(plugin, System.getenv("LUCKPERMS_FILE_SECRET_DIRECTORY")); + } + + @Override + protected @Nullable String resolveValue(String path) { + if (this.directory == null) { + return null; // not configured + } + + // e.g. + // 'server' -> luckperms_server + // 'data.table_prefix' -> luckperms_data_table_prefix + String key = PREFIX + path.toLowerCase(Locale.ROOT) + .replace('-', '_') + .replace('.', '_'); + + Path resolvedFile = this.directory.resolve(key); + + String value; + try { + value = new String(Files.readAllBytes(resolvedFile), StandardCharsets.UTF_8); + } catch (IOException e) { + return null; + } + + String printableValue = ConfigKeys.shouldCensorValue(path) ? "*****" : value; + this.plugin.getLogger().info(String.format("Resolved configuration value from file secret: %s = %s", key, printableValue)); + return value; + } + + @Override + public LuckPermsPlugin getPlugin() { + return this.plugin; + } + + @Override + public void reload() { + // no-op + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/MultiConfigurationAdapter.java b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/MultiConfigurationAdapter.java new file mode 100644 index 000000000..2c4f965f9 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/MultiConfigurationAdapter.java @@ -0,0 +1,116 @@ +/* + * 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.config.generic.adapter; + +import com.google.common.collect.ImmutableList; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; + +import java.util.List; +import java.util.Map; + +/** + * A {@link ConfigurationAdapter} composed of one or more other ConfigurationAdapters. + */ +public class MultiConfigurationAdapter implements ConfigurationAdapter { + private final LuckPermsPlugin plugin; + private final List adapters; + + /** + * Creates a {@link MultiConfigurationAdapter}. + * + *

    The first adapter in the list has priority (the final say) in deciding what the value is. + * All adapters are tried in reverse order, and the value returned from the previous adapter + * is passed into the next as the {@code def} value.

    + * + * @param plugin the plugin + * @param adapters a list of adapters + */ + public MultiConfigurationAdapter(LuckPermsPlugin plugin, List adapters) { + this.plugin = plugin; + this.adapters = ImmutableList.copyOf(adapters).reverse(); + } + + public MultiConfigurationAdapter(LuckPermsPlugin plugin, ConfigurationAdapter... adapters) { + this(plugin, ImmutableList.copyOf(adapters)); + } + + @Override + public LuckPermsPlugin getPlugin() { + return this.plugin; + } + + @Override + public void reload() { + for (ConfigurationAdapter adapter : this.adapters) { + adapter.reload(); + } + } + + @Override + public String getString(String path, String def) { + String result = def; + for (ConfigurationAdapter adapter : this.adapters) { + result = adapter.getString(path, result); + } + return result; + } + + @Override + public int getInteger(String path, int def) { + int result = def; + for (ConfigurationAdapter adapter : this.adapters) { + result = adapter.getInteger(path, result); + } + return result; + } + + @Override + public boolean getBoolean(String path, boolean def) { + boolean result = def; + for (ConfigurationAdapter adapter : this.adapters) { + result = adapter.getBoolean(path, result); + } + return result; + } + + @Override + public List getStringList(String path, List def) { + List result = def; + for (ConfigurationAdapter adapter : this.adapters) { + result = adapter.getStringList(path, result); + } + return result; + } + + @Override + public Map getStringMap(String path, Map def) { + Map result = def; + for (ConfigurationAdapter adapter : this.adapters) { + result = adapter.getStringMap(path, result); + } + return result; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/StringBasedConfigurationAdapter.java b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/StringBasedConfigurationAdapter.java new file mode 100644 index 000000000..a1b81daa9 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/StringBasedConfigurationAdapter.java @@ -0,0 +1,98 @@ +/* + * 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.config.generic.adapter; + +import com.google.common.base.Splitter; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.List; +import java.util.Map; + +public abstract class StringBasedConfigurationAdapter implements ConfigurationAdapter { + + private static final Splitter LIST_SPLITTER = Splitter.on(','); + private static final Splitter.MapSplitter MAP_SPLITTER = Splitter.on(',').withKeyValueSeparator('='); + + protected abstract @Nullable String resolveValue(String path); + + @Override + public String getString(String path, String def) { + String value = resolveValue(path); + if (value == null) { + return def; + } + + return value; + } + + @Override + public int getInteger(String path, int def) { + String value = resolveValue(path); + if (value == null) { + return def; + } + + try { + return Integer.parseInt(value); + } catch (IllegalArgumentException e) { + return def; + } + } + + @Override + public boolean getBoolean(String path, boolean def) { + String value = resolveValue(path); + if (value == null) { + return def; + } + + try { + return Boolean.parseBoolean(value); + } catch (IllegalArgumentException e) { + return def; + } + } + + @Override + public List getStringList(String path, List def) { + String value = resolveValue(path); + if (value == null) { + return def; + } + + return LIST_SPLITTER.splitToList(value); + } + + @Override + public Map getStringMap(String path, Map def) { + String value = resolveValue(path); + if (value == null) { + return def; + } + + return MAP_SPLITTER.split(value); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/SystemPropertyConfigAdapter.java b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/SystemPropertyConfigAdapter.java new file mode 100644 index 000000000..dfe369c0f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/config/generic/adapter/SystemPropertyConfigAdapter.java @@ -0,0 +1,65 @@ +/* + * 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.config.generic.adapter; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class SystemPropertyConfigAdapter extends StringBasedConfigurationAdapter { + private static final String PREFIX = "luckperms."; + + private final LuckPermsPlugin plugin; + + public SystemPropertyConfigAdapter(LuckPermsPlugin plugin) { + this.plugin = plugin; + } + + @Override + protected @Nullable String resolveValue(String path) { + // e.g. + // 'server' -> luckperms.server + // 'data.table_prefix' -> luckperms.data.table-prefix + String key = PREFIX + path; + + String value = System.getProperty(key); + if (value != null) { + String printableValue = ConfigKeys.shouldCensorValue(path) ? "*****" : value; + this.plugin.getLogger().info(String.format("Resolved configuration value from system property: %s = %s", key, printableValue)); + } + return value; + } + + @Override + public LuckPermsPlugin getPlugin() { + return this.plugin; + } + + @Override + public void reload() { + // no-op + } +} 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 58afb78b3..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 @@ -26,9 +26,10 @@ package me.lucko.luckperms.common.config.generic.key; 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,7 +37,8 @@ public interface ConfigKeyFactory { ConfigKeyFactory BOOLEAN = ConfigurationAdapter::getBoolean; ConfigKeyFactory STRING = ConfigurationAdapter::getString; - ConfigKeyFactory LOWERCASE_STRING = (adapter, path, def) -> adapter.getString(path, def).toLowerCase(); + 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())); static SimpleConfigKey key(Function function) { @@ -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/context/contextset/AbstractContextSet.java b/common/src/main/java/me/lucko/luckperms/common/context/AbstractContextSet.java similarity index 67% rename from common/src/main/java/me/lucko/luckperms/common/context/contextset/AbstractContextSet.java rename to common/src/main/java/me/lucko/luckperms/common/context/AbstractContextSet.java index 87388ce5e..bd8346c98 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/contextset/AbstractContextSet.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/AbstractContextSet.java @@ -23,48 +23,19 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context.contextset; - -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterators; -import com.google.common.collect.SetMultimap; +package me.lucko.luckperms.common.context; import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextSatisfyMode; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; - import org.checkerframework.checker.nullness.qual.NonNull; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; +import java.util.Locale; import java.util.Objects; -import java.util.Set; -import java.util.Spliterator; public abstract class AbstractContextSet implements ContextSet { - protected abstract SetMultimap backing(); - - protected abstract void copyTo(SetMultimap other); - - @Override - public boolean containsKey(@NonNull String key) { - return backing().containsKey(sanitizeKey(key)); - } - - @Override - public @NonNull Set getValues(@NonNull String key) { - Collection values = backing().asMap().get(sanitizeKey(key)); - return values != null ? ImmutableSet.copyOf(values) : ImmutableSet.of(); - } - - @Override - public boolean contains(@NonNull String key, @NonNull String value) { - return backing().containsEntry(sanitizeKey(key), sanitizeValue(value)); - } - @Override public boolean isSatisfiedBy(@NonNull ContextSet other, @NonNull ContextSatisfyMode mode) { if (this == other) { @@ -95,39 +66,12 @@ public boolean isSatisfiedBy(@NonNull ContextSet other, @NonNull ContextSatisfyM protected abstract boolean otherContainsAll(ContextSet other, ContextSatisfyMode mode); - public abstract Context[] toArray(); - - @Override - public @NonNull Iterator iterator() { - return Iterators.forArray(toArray()); - } - - @Override - public Spliterator spliterator() { - return Arrays.spliterator(toArray()); - } - - @Override - public boolean isEmpty() { - return backing().isEmpty(); - } - - @Override - public int size() { - return backing().size(); - } - - @Override - public int hashCode() { - return backing().hashCode(); - } - static String sanitizeKey(String key) { Objects.requireNonNull(key, "key is null"); if (!Context.isValidKey(key)) { throw new IllegalArgumentException("key is (effectively) empty"); } - return key.toLowerCase(); + return key.toLowerCase(Locale.ROOT); } static String sanitizeValue(String value) { @@ -135,7 +79,7 @@ static String sanitizeValue(String value) { if (!Context.isValidValue(value)) { throw new IllegalArgumentException("value is (effectively) empty"); } - return value.toLowerCase(); + return value.toLowerCase(Locale.ROOT); } public static boolean isGlobalServerWorldEntry(String key, String value) { diff --git a/common/src/main/java/me/lucko/luckperms/common/context/contextset/ContextImpl.java b/common/src/main/java/me/lucko/luckperms/common/context/ContextImpl.java similarity index 82% rename from common/src/main/java/me/lucko/luckperms/common/context/contextset/ContextImpl.java rename to common/src/main/java/me/lucko/luckperms/common/context/ContextImpl.java index 4e7c5a13d..23c238331 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/contextset/ContextImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/ContextImpl.java @@ -23,13 +23,13 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context.contextset; +package me.lucko.luckperms.common.context; +import me.lucko.luckperms.common.context.comparator.ContextComparator; import net.luckperms.api.context.Context; - import org.checkerframework.checker.nullness.qual.NonNull; -public final class ContextImpl implements Context { +public final class ContextImpl implements Context, Comparable { private final String key; private final String value; @@ -48,8 +48,14 @@ public ContextImpl(String key, String value) { return this.value; } + @Override + public int compareTo(@NonNull Context o) { + return ContextComparator.INSTANCE.compare(this, o); + } + @Override public boolean equals(Object obj) { + if (this == obj) return true; if (!(obj instanceof Context)) return false; Context that = (Context) obj; return this.key.equals(that.getKey()) && this.value.equals(that.getValue()); @@ -59,4 +65,9 @@ public boolean equals(Object obj) { public int hashCode() { return this.key.hashCode() ^ this.value.hashCode(); } + + @Override + public String toString() { + return this.key + '=' + this.value; + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/context/contextset/ImmutableContextSetImpl.java b/common/src/main/java/me/lucko/luckperms/common/context/ImmutableContextSetImpl.java similarity index 52% rename from common/src/main/java/me/lucko/luckperms/common/context/contextset/ImmutableContextSetImpl.java rename to common/src/main/java/me/lucko/luckperms/common/context/ImmutableContextSetImpl.java index 862d30ec6..4f9153aff 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/contextset/ImmutableContextSetImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/ImmutableContextSetImpl.java @@ -23,33 +23,31 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context.contextset; +package me.lucko.luckperms.common.context; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.Iterators; import com.google.common.collect.Multimaps; -import com.google.common.collect.SetMultimap; - -import me.lucko.luckperms.common.context.ContextSetComparator; - +import me.lucko.luckperms.common.context.comparator.ContextComparator; import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextSatisfyMode; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.context.MutableContextSet; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.Spliterator; -public final class ImmutableContextSetImpl extends AbstractContextSet implements ImmutableContextSet { - public static final ImmutableContextSetImpl EMPTY = new ImmutableContextSetImpl(ImmutableSetMultimap.of()); +public final class ImmutableContextSetImpl extends AbstractContextSet implements ImmutableContextSet, ContextSet { + public static final ImmutableContextSetImpl EMPTY = new ImmutableContextSetImpl(new Context[0]); public static ImmutableContextSet of(String key, String value) { key = sanitizeKey(key); @@ -60,35 +58,19 @@ public static ImmutableContextSet of(String key, String value) { return EMPTY; } - return new ImmutableContextSetImpl(ImmutableSetMultimap.of(key, sanitizeValue(value))); + return new ImmutableContextSetImpl(new Context[]{new ContextImpl(key, value)}); } - private final ImmutableSetMultimap map; private final Context[] array; + private final int size; private final int hashCode; - ImmutableContextSetImpl(ImmutableSetMultimap contexts) { - this.map = contexts; - this.hashCode = this.map.hashCode(); - - Set> entries = this.map.entries(); - this.array = new Context[entries.size()]; - int i = 0; - for (Map.Entry e : entries) { - this.array[i++] = new ContextImpl(e.getKey(), e.getValue()); - } - // sort the array at construction so the comparator doesn't need to - Arrays.sort(this.array, ContextSetComparator.CONTEXT_COMPARATOR); - } - - @Override - protected SetMultimap backing() { - return this.map; - } + private ImmutableSetMultimap cachedMap; - @Override - protected void copyTo(SetMultimap other) { - other.putAll(this.map); + ImmutableContextSetImpl(Context[] contexts) { + this.array = contexts; // always sorted + this.size = this.array.length; + this.hashCode = Arrays.hashCode(this.array); } @Override @@ -102,9 +84,20 @@ public boolean isImmutable() { return this; } + public ImmutableSetMultimap toMultimap() { + if (this.cachedMap == null) { + ImmutableSetMultimap.Builder builder = ImmutableSetMultimap.builder(); + for (Context entry : this.array) { + builder.put(entry.getKey(), entry.getValue()); + } + this.cachedMap = builder.build(); + } + return this.cachedMap; + } + @Override public @NonNull MutableContextSet mutableCopy() { - return new MutableContextSetImpl(this.map); + return new MutableContextSetImpl(toMultimap()); } @Override @@ -114,22 +107,21 @@ public boolean isImmutable() { @Override public @NonNull Map> toMap() { - return Multimaps.asMap(this.map); + return Multimaps.asMap(toMultimap()); } @Deprecated @Override public @NonNull Map toFlattenedMap() { ImmutableMap.Builder m = ImmutableMap.builder(); - for (Map.Entry e : this.map.entries()) { + for (Context e : this.array) { m.put(e.getKey(), e.getValue()); } return m.build(); } - @Override public Context[] toArray() { - return this.array; + return this.array; // only used read-only & internally } @Override @@ -137,8 +129,7 @@ protected boolean otherContainsAll(ContextSet other, ContextSatisfyMode mode) { switch (mode) { // Use other.contains case ALL_VALUES_PER_KEY: { - Set> entries = this.map.entries(); - for (Map.Entry e : entries) { + for (Context e : this.array) { if (!other.contains(e.getKey(), e.getValue())) { return false; } @@ -148,12 +139,26 @@ protected boolean otherContainsAll(ContextSet other, ContextSatisfyMode mode) { // Use other.containsAny case AT_LEAST_ONE_VALUE_PER_KEY: { - Set>> entries = this.map.asMap().entrySet(); - for (Map.Entry> e : entries) { - if (!other.containsAny(e.getKey(), e.getValue())) { - return false; + // exploit the ordered nature to only scan through the array once. + Context[] array = this.array; + for (int i = 0, len = array.length; i < len; i++) { + Context e = array[i]; + + boolean otherContains = other.contains(e.getKey(), e.getValue()); + if (otherContains) { + // skip forward past any other entries with the same key + while (i+1 < len && array[i+1].getKey().equals(e.getKey())) { + i++; + } + } else { + // if this is the last one of the key, return false + int next = i + 1; + if (next >= len || !array[next].getKey().equals(e.getKey())) { + return false; + } } } + return true; } default: @@ -171,21 +176,10 @@ public boolean equals(Object o) { if (that instanceof ImmutableContextSetImpl) { ImmutableContextSetImpl immutableThat = (ImmutableContextSetImpl) that; if (this.hashCode != immutableThat.hashCode) return false; + return Arrays.equals(this.array, immutableThat.array); } - final Multimap thatBacking; - if (that instanceof AbstractContextSet) { - thatBacking = ((AbstractContextSet) that).backing(); - } else { - Map> thatMap = that.toMap(); - ImmutableSetMultimap.Builder thatBuilder = ImmutableSetMultimap.builder(); - for (Map.Entry> e : thatMap.entrySet()) { - thatBuilder.putAll(e.getKey(), e.getValue()); - } - thatBacking = thatBuilder.build(); - } - - return backing().equals(thatBacking); + return this.size() == that.size() && otherContainsAll(that, ContextSatisfyMode.ALL_VALUES_PER_KEY); } @Override @@ -195,57 +189,116 @@ public int hashCode() { @Override public String toString() { - return "ImmutableContextSet(contexts=" + this.map + ")"; + return "ImmutableContextSet(" + Arrays.toString(this.array) + ")"; + } + + @Override + public boolean containsKey(@NonNull String key) { + Objects.requireNonNull(key, "key"); + return Arrays.binarySearch(this.array, new ContextImpl(key, null), ContextComparator.ONLY_KEY) >= 0; + } + + @Override + public @NonNull Set getValues(@NonNull String key) { + Collection values = toMap().get(sanitizeKey(key)); + return values != null ? ImmutableSet.copyOf(values) : ImmutableSet.of(); + } + + @Override + public boolean contains(@NonNull Context entry) { + Objects.requireNonNull(entry, "entry"); + return Arrays.binarySearch(this.array, entry) >= 0; + } + + @Override + public boolean contains(@NonNull String key, @NonNull String value) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(value, "value"); + return contains(new ContextImpl(key, value)); + } + + @Override + public @NonNull Iterator iterator() { + return Iterators.forArray(this.array); + } + + @Override + public Spliterator spliterator() { + return Arrays.spliterator(this.array); + } + + @Override + public boolean isEmpty() { + return this.size == 0; + } + + @Override + public int size() { + return this.size; } public static final class BuilderImpl implements ImmutableContextSet.Builder { - private ImmutableSetMultimap.Builder builder; + private static final int INITIAL_SIZE = 16; + private Context[] builder = EMPTY.array; + private int size = 0; public BuilderImpl() { } - private synchronized ImmutableSetMultimap.Builder builder() { - if (this.builder == null) { - this.builder = ImmutableSetMultimap.builder(); - } - return this.builder; - } - private void put(String key, String value) { - // special case for server=global and world=global - if (isGlobalServerWorldEntry(key, value)) { + ContextImpl context = new ContextImpl(key, value); + + int pos = Arrays.binarySearch(this.builder, 0, this.size, context); + if (pos >= 0) { return; } - builder().put(key, value); + + int insertPos = -pos - 1; + + Context[] dest; + if (this.builder.length == this.size) { + // grow + dest = new Context[Math.max(this.builder.length * 2, INITIAL_SIZE)]; + System.arraycopy(this.builder, 0, dest, 0, insertPos); + } else { + dest = this.builder; + } + + System.arraycopy(this.builder, insertPos, dest, insertPos + 1, this.size - insertPos); // shift + dest[insertPos] = context; // insert + + this.size++; + this.builder = dest; } @Override public @NonNull BuilderImpl add(@NonNull String key, @NonNull String value) { - put(sanitizeKey(key), sanitizeValue(value)); + key = sanitizeKey(key); + value = sanitizeValue(value); + + // special case for server=global and world=global + if (isGlobalServerWorldEntry(key, value)) { + return this; + } + + put(key, value); return this; } @Override public @NonNull BuilderImpl addAll(@NonNull ContextSet contextSet) { Objects.requireNonNull(contextSet, "contextSet"); - if (contextSet instanceof AbstractContextSet) { - AbstractContextSet other = (AbstractContextSet) contextSet; - if (!other.isEmpty()) { - builder().putAll(other.backing()); - } - } else { - addAll(contextSet.toSet()); - } + addAll(contextSet.toSet()); return this; } @Override public @NonNull ImmutableContextSet build() { - if (this.builder == null) { + if (this.builder.length == 0) { return EMPTY; } else { - return new ImmutableContextSetImpl(this.builder.build()); + return new ImmutableContextSetImpl(Arrays.copyOf(this.builder, this.size)); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/context/contextset/MutableContextSetImpl.java b/common/src/main/java/me/lucko/luckperms/common/context/MutableContextSetImpl.java similarity index 81% rename from common/src/main/java/me/lucko/luckperms/common/context/contextset/MutableContextSetImpl.java rename to common/src/main/java/me/lucko/luckperms/common/context/MutableContextSetImpl.java index 76a2c86cb..667fd53e6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/contextset/MutableContextSetImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/MutableContextSetImpl.java @@ -23,30 +23,30 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context.contextset; +package me.lucko.luckperms.common.context; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSetMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.Iterators; import com.google.common.collect.Multimaps; import com.google.common.collect.SetMultimap; - import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextSatisfyMode; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.context.MutableContextSet; - import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.Spliterator; -public final class MutableContextSetImpl extends AbstractContextSet implements MutableContextSet { +public final class MutableContextSetImpl extends AbstractContextSet implements MutableContextSet, ContextSet { private final SetMultimap map; public MutableContextSetImpl() { @@ -57,18 +57,6 @@ public MutableContextSetImpl() { this.map = Multimaps.synchronizedSetMultimap(HashMultimap.create(other)); } - @Override - protected SetMultimap backing() { - return this.map; - } - - @Override - protected void copyTo(SetMultimap other) { - synchronized (this.map) { - other.putAll(this.map); - } - } - @Override public boolean isImmutable() { return false; @@ -80,9 +68,10 @@ public boolean isImmutable() { if (this.map.isEmpty()) { return ImmutableContextSetImpl.EMPTY; } - synchronized (this.map) { - return new ImmutableContextSetImpl(ImmutableSetMultimap.copyOf(this.map)); - } + + Context[] arr = toArray(); + Arrays.sort(arr); + return new ImmutableContextSetImpl(arr); } @Override @@ -129,11 +118,10 @@ public boolean isImmutable() { return builder.build(); } - @Override public Context[] toArray() { - Set> entries = this.map.entries(); Context[] array; synchronized (this.map) { + Set> entries = this.map.entries(); array = new Context[entries.size()]; int i = 0; for (Map.Entry e : entries) { @@ -143,6 +131,42 @@ public Context[] toArray() { return array; } + @Override + public boolean containsKey(@NonNull String key) { + return this.map.containsKey(sanitizeKey(key)); + } + + @Override + public @NonNull Set getValues(@NonNull String key) { + Collection values = this.map.asMap().get(sanitizeKey(key)); + return values != null ? ImmutableSet.copyOf(values) : ImmutableSet.of(); + } + + @Override + public boolean contains(@NonNull String key, @NonNull String value) { + return this.map.containsEntry(sanitizeKey(key), sanitizeValue(value)); + } + + @Override + public @NonNull Iterator iterator() { + return Iterators.forArray(toArray()); + } + + @Override + public Spliterator spliterator() { + return Arrays.spliterator(toArray()); + } + + @Override + public boolean isEmpty() { + return this.map.isEmpty(); + } + + @Override + public int size() { + return this.map.size(); + } + @Override public void add(@NonNull String key, @NonNull String value) { key = sanitizeKey(key); @@ -159,12 +183,7 @@ public void add(@NonNull String key, @NonNull String value) { @Override public void addAll(@NonNull ContextSet contextSet) { Objects.requireNonNull(contextSet, "contextSet"); - if (contextSet instanceof AbstractContextSet) { - AbstractContextSet other = (AbstractContextSet) contextSet; - other.copyTo(this.map); - } else { - addAll(contextSet.toSet()); - } + addAll(contextSet.toSet()); } @Override @@ -221,23 +240,16 @@ public boolean equals(Object o) { if (!(o instanceof ContextSet)) return false; final ContextSet that = (ContextSet) o; - final Multimap thatBacking; - if (that instanceof AbstractContextSet) { - thatBacking = ((AbstractContextSet) that).backing(); - } else { - Map> thatMap = that.toMap(); - ImmutableSetMultimap.Builder thatBuilder = ImmutableSetMultimap.builder(); - for (Map.Entry> e : thatMap.entrySet()) { - thatBuilder.putAll(e.getKey(), e.getValue()); - } - thatBacking = thatBuilder.build(); - } + return this.size() == that.size() && otherContainsAll(that, ContextSatisfyMode.ALL_VALUES_PER_KEY); + } - return backing().equals(thatBacking); + @Override + public int hashCode() { + return this.map.hashCode(); } @Override public String toString() { - return "MutableContextSet(contexts=" + this.map + ")"; + return "MutableContextSet(" + this.map + ")"; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/context/ConfigurationContextCalculator.java b/common/src/main/java/me/lucko/luckperms/common/context/calculator/ConfigurationContextCalculator.java similarity index 95% rename from common/src/main/java/me/lucko/luckperms/common/context/ConfigurationContextCalculator.java rename to common/src/main/java/me/lucko/luckperms/common/context/calculator/ConfigurationContextCalculator.java index d499ce499..9665a7ee9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/ConfigurationContextCalculator.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/calculator/ConfigurationContextCalculator.java @@ -23,18 +23,16 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context; +package me.lucko.luckperms.common.context.calculator; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.LuckPermsConfiguration; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; - +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.context.StaticContextCalculator; - import org.checkerframework.checker.nullness.qual.NonNull; public class ConfigurationContextCalculator implements StaticContextCalculator { diff --git a/common/src/main/java/me/lucko/luckperms/common/context/ForwardingContextCalculator.java b/common/src/main/java/me/lucko/luckperms/common/context/calculator/ForwardingContextCalculator.java similarity index 96% rename from common/src/main/java/me/lucko/luckperms/common/context/ForwardingContextCalculator.java rename to common/src/main/java/me/lucko/luckperms/common/context/calculator/ForwardingContextCalculator.java index 0b3a99c0a..187db01ae 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/ForwardingContextCalculator.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/calculator/ForwardingContextCalculator.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context; +package me.lucko.luckperms.common.context.calculator; import net.luckperms.api.context.ContextCalculator; diff --git a/common/src/main/java/me/lucko/luckperms/common/context/WorldNameRewriter.java b/common/src/main/java/me/lucko/luckperms/common/context/calculator/WorldNameRewriter.java similarity index 95% rename from common/src/main/java/me/lucko/luckperms/common/context/WorldNameRewriter.java rename to common/src/main/java/me/lucko/luckperms/common/context/calculator/WorldNameRewriter.java index 148ee01d4..3a2e865df 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/WorldNameRewriter.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/calculator/WorldNameRewriter.java @@ -23,15 +23,15 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context; +package me.lucko.luckperms.common.context.calculator; import me.lucko.luckperms.common.config.ConfigKeys; - import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.DefaultContextKeys; import java.util.HashSet; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -69,7 +69,7 @@ public NonEmpty(Map rewrites) { @Override public void rewriteAndSubmit(String worldName, ContextConsumer consumer) { Set seen = new HashSet<>(); - worldName = worldName.toLowerCase(); + worldName = worldName.toLowerCase(Locale.ROOT); while (Context.isValidValue(worldName) && seen.add(worldName)) { consumer.accept(DefaultContextKeys.WORLD_KEY, worldName); diff --git a/common/src/main/java/me/lucko/luckperms/common/context/comparator/ContextComparator.java b/common/src/main/java/me/lucko/luckperms/common/context/comparator/ContextComparator.java new file mode 100644 index 000000000..bec4d7efc --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/context/comparator/ContextComparator.java @@ -0,0 +1,74 @@ +/* + * 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.context.comparator; + +import net.luckperms.api.context.Context; + +import java.util.Comparator; + +public class ContextComparator implements Comparator { + + public static final ContextComparator INSTANCE = new ContextComparator(false); + public static final ContextComparator ONLY_KEY = new ContextComparator(true); + + private final boolean onlyKeys; + + public ContextComparator(boolean onlyKeys) { + this.onlyKeys = onlyKeys; + } + + @Override + public int compare(Context o1, Context o2) { + if (o1 == o2) { + return 0; + } + + int i = compareStringsFast(o1.getKey(), o2.getKey()); + if (i != 0) { + return i; + } + + if (this.onlyKeys) { + return 0; + } + + return compareStringsFast(o1.getValue(), o2.getValue()); + } + + public int compare(Context o1, String o2Key, String o2Value) { + int i = compareStringsFast(o1.getKey(), o2Key); + if (i != 0) { + return i; + } + + return compareStringsFast(o1.getValue(), o2Value); + } + + @SuppressWarnings("StringEquality") + private static int compareStringsFast(String o1, String o2) { + return o1 == o2 ? 0 : o1.compareTo(o2); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/context/ContextSetComparator.java b/common/src/main/java/me/lucko/luckperms/common/context/comparator/ContextSetComparator.java similarity index 76% rename from common/src/main/java/me/lucko/luckperms/common/context/ContextSetComparator.java rename to common/src/main/java/me/lucko/luckperms/common/context/comparator/ContextSetComparator.java index 66eb8e132..29fe71f7c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/ContextSetComparator.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/comparator/ContextSetComparator.java @@ -23,10 +23,9 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context; - -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +package me.lucko.luckperms.common.context.comparator; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import net.luckperms.api.context.Context; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; @@ -35,16 +34,15 @@ import java.util.Comparator; public class ContextSetComparator implements Comparator { + private static final Comparator ASCENDING = new ContextSetComparator(); + private static final Comparator DESCENDING = ASCENDING.reversed(); - private static final Comparator INSTANCE = new ContextSetComparator(); - private static final Comparator REVERSE = INSTANCE.reversed(); - - public static Comparator normal() { - return INSTANCE; + public static Comparator ascending() { + return ASCENDING; } - public static Comparator reverse() { - return REVERSE; + public static Comparator descending() { + return DESCENDING; } @Override @@ -90,7 +88,7 @@ public int compare(ImmutableContextSet o1, ImmutableContextSet o2) { Context ent1 = o1Array[i]; Context ent2 = o2Array[i]; - result = compareContexts(ent1, ent2); + result = ContextComparator.INSTANCE.compare(ent1, ent2); if (result != 0) { return result; } @@ -99,29 +97,10 @@ public int compare(ImmutableContextSet o1, ImmutableContextSet o2) { throw new AssertionError("sets are equal? " + o1 + " - " + o2); } - public static final Comparator CONTEXT_COMPARATOR = ContextSetComparator::compareContexts; - private static Context[] toArray(ImmutableContextSet set) { Context[] array = set.toSet().toArray(new Context[0]); - Arrays.sort(array, CONTEXT_COMPARATOR); + Arrays.sort(array, ContextComparator.INSTANCE); return array; } - private static int compareContexts(Context o1, Context o2) { - if (o1 == o2) { - return 0; - } - - int i = compareStringsFast(o1.getKey(), o2.getKey()); - if (i != 0) { - return i; - } - - return compareStringsFast(o1.getValue(), o2.getValue()); - } - - @SuppressWarnings("StringEquality") - private static int compareStringsFast(String o1, String o2) { - return o1 == o2 ? 0 : o1.compareTo(o2); - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/context/manager/ContextManager.java b/common/src/main/java/me/lucko/luckperms/common/context/manager/ContextManager.java new file mode 100644 index 000000000..a42b45bbe --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/context/manager/ContextManager.java @@ -0,0 +1,65 @@ +/* + * 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.context.manager; + +import net.luckperms.api.context.ContextCalculator; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.query.QueryOptions; + +import java.util.UUID; + +/** + * Manages contexts for subjects. + * + * @param the subject type + * @param

    the player type + */ +public interface ContextManager { + + QueryOptions getQueryOptions(S subject); + + ImmutableContextSet getContext(S subject); + + QueryOptions getStaticQueryOptions(); + + ImmutableContextSet getStaticContext(); + + UUID getUniqueId(P player); + + void signalContextUpdate(S subject); + + void invalidateCache(S subject); + + void registerCalculator(ContextCalculator calculator); + + void unregisterCalculator(ContextCalculator calculator); + + ImmutableContextSet getPotentialContexts(); + + Class getSubjectClass(); + + Class

    getPlayerClass(); +} diff --git a/common/src/main/java/me/lucko/luckperms/common/context/ContextManager.java b/common/src/main/java/me/lucko/luckperms/common/context/manager/ContextManagerBase.java similarity index 72% rename from common/src/main/java/me/lucko/luckperms/common/context/ContextManager.java rename to common/src/main/java/me/lucko/luckperms/common/context/manager/ContextManagerBase.java index 779763ae2..7125cfd2d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/ContextManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/manager/ContextManagerBase.java @@ -23,33 +23,34 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context; +package me.lucko.luckperms.common.context.manager; import me.lucko.luckperms.common.cache.ExpiringCache; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.calculator.ForwardingContextCalculator; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.context.ContextCalculator; import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.context.StaticContextCalculator; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.ArrayList; import java.util.List; -import java.util.UUID; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; /** - * Base implementation of {@link ContextManager} which caches content lookups. + * Base implementation of {@link ContextManager}. * - * @param the calculator type + * @param the subject type + * @param

    the player type */ -public abstract class ContextManager { +public abstract class ContextManagerBase implements ContextManager { protected final LuckPermsPlugin plugin; private final Class subjectClass; @@ -60,46 +61,46 @@ public abstract class ContextManager { // caches static context lookups private final StaticLookupCache staticLookupCache = new StaticLookupCache(); - protected ContextManager(LuckPermsPlugin plugin, Class subjectClass, Class

    playerClass) { + protected ContextManagerBase(LuckPermsPlugin plugin, Class subjectClass, Class

    playerClass) { this.plugin = plugin; this.subjectClass = subjectClass; this.playerClass = playerClass; } + @Override public Class getSubjectClass() { return this.subjectClass; } + @Override public Class

    getPlayerClass() { return this.playerClass; } - public abstract UUID getUniqueId(P player); - - public abstract QueryOptionsSupplier getCacheFor(S subject); - - public QueryOptions getQueryOptions(S subject) { - return getCacheFor(subject).getQueryOptions(); - } - + @Override public ImmutableContextSet getContext(S subject) { - return getCacheFor(subject).getContextSet(); + return getQueryOptions(subject).context(); } + @Override public QueryOptions getStaticQueryOptions() { return this.staticLookupCache.get(); } + @Override public ImmutableContextSet getStaticContext() { return getStaticQueryOptions().context(); } - public QueryOptions formQueryOptions(ImmutableContextSet contextSet) { - return this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder().context(contextSet).build(); + protected void customizeStaticQueryOptions(QueryOptions.Builder builder) { + // overridden } - public abstract QueryOptions formQueryOptions(S subject, ImmutableContextSet contextSet); + protected void customizeQueryOptions(S subject, QueryOptions.Builder builder) { + // overridden + } + @Override public void signalContextUpdate(S subject) { if (subject == null) { throw new NullPointerException("subject"); @@ -112,29 +113,53 @@ public void signalContextUpdate(S subject) { this.plugin.getEventDispatcher().dispatchContextUpdate(subject); } - protected abstract void invalidateCache(S subject); - + @Override public void registerCalculator(ContextCalculator calculator) { + String calculatorClass = calculator.getClass().getName(); + + Set> disabledCalculators = this.plugin.getConfiguration().get(ConfigKeys.DISABLED_CONTEXT_CALCULATORS); + for (Predicate disabledPattern : disabledCalculators) { + if (disabledPattern.test(calculatorClass)) { + this.plugin.getLogger().info("Ignoring registration of disabled context calculator: " + calculatorClass); + return; + } + } + this.calculators.add(calculator); } + @Override public void unregisterCalculator(ContextCalculator calculator) { this.calculators.remove(calculator); } + protected void callContextCalculator(ContextCalculator calculator, S subject, ContextConsumer consumer) { + try { + calculator.calculate(subject, consumer); + } catch (Throwable e) { + this.plugin.getLogger().warn("An exception was thrown by " + getCalculatorClass(calculator) + " whilst calculating the context of subject " + subject, e); + } + } + + protected void callStaticContextCalculator(StaticContextCalculator calculator, ContextConsumer consumer) { + try { + calculator.calculate(consumer); + } catch (Throwable e) { + this.plugin.getLogger().warn("An exception was thrown by " + getCalculatorClass(calculator) + " whilst calculating static contexts", e); + } + } + protected QueryOptions calculate(S subject) { ImmutableContextSet.Builder accumulator = new ImmutableContextSetImpl.BuilderImpl(); ContextConsumer consumer = accumulator::add; for (ContextCalculator calculator : this.calculators.calculators()) { - try { - calculator.calculate(subject, consumer); - } catch (Throwable e) { - this.plugin.getLogger().warn("An exception was thrown by " + getCalculatorClass(calculator) + " whilst calculating the context of subject " + subject, e); - } + callContextCalculator(calculator, subject, consumer); } - return formQueryOptions(subject, accumulator.build()); + QueryOptions.Builder builder = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder().context(accumulator.build()); + customizeQueryOptions(subject, builder); + return builder.build(); } private QueryOptions calculateStatic() { @@ -142,16 +167,15 @@ private QueryOptions calculateStatic() { ContextConsumer consumer = accumulator::add; for (StaticContextCalculator calculator : this.calculators.staticCalculators()) { - try { - calculator.calculate(consumer); - } catch (Throwable e) { - this.plugin.getLogger().warn("An exception was thrown by " + getCalculatorClass(calculator) + " whilst calculating static contexts", e); - } + callStaticContextCalculator(calculator, consumer); } - return formQueryOptions(accumulator.build()); + QueryOptions.Builder builder = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder().context(accumulator.build()); + customizeStaticQueryOptions(builder); + return builder.build(); } + @Override public ImmutableContextSet getPotentialContexts() { ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); diff --git a/common/src/main/java/me/lucko/luckperms/common/context/manager/DetachedContextManager.java b/common/src/main/java/me/lucko/luckperms/common/context/manager/DetachedContextManager.java new file mode 100644 index 000000000..cf0e904b3 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/context/manager/DetachedContextManager.java @@ -0,0 +1,73 @@ +/* + * 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.context.manager; + +import com.github.benmanes.caffeine.cache.LoadingCache; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.util.CaffeineFactory; +import net.luckperms.api.query.QueryOptions; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.concurrent.TimeUnit; + +/** + * Implementation of {@link ContextManagerBase} which utilises 'detached' supplier caches stored alongside the subject instances. + */ +public abstract class DetachedContextManager extends ContextManagerBase { + + private final LoadingCache fallbackContextsCache = CaffeineFactory.newBuilder() + .expireAfterWrite(50, TimeUnit.MILLISECONDS) + .build(this::calculate); + + protected DetachedContextManager(LuckPermsPlugin plugin, Class subjectClass, Class

    playerClass) { + super(plugin, subjectClass, playerClass); + } + + @Override + public QueryOptions getQueryOptions(S subject) { + QueryOptionsSupplier supplier = getQueryOptionsSupplier(subject); + if (supplier != null) { + return supplier.getQueryOptions(); + } + return this.fallbackContextsCache.get(subject); + } + + @Override + public void invalidateCache(S subject) { + QueryOptionsSupplier queryOptionsSupplier = getQueryOptionsSupplier(subject); + if (queryOptionsSupplier != null) { + queryOptionsSupplier.invalidateCache(); + } + this.fallbackContextsCache.invalidate(subject); + } + + public QueryOptionsSupplier createQueryOptionsSupplier(S subject) { + return new QueryOptionsCache<>(subject, this); + } + + public abstract @Nullable QueryOptionsSupplier getQueryOptionsSupplier(S subject); + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/context/QueryOptionsCache.java b/common/src/main/java/me/lucko/luckperms/common/context/manager/QueryOptionsCache.java similarity index 85% rename from common/src/main/java/me/lucko/luckperms/common/context/QueryOptionsCache.java rename to common/src/main/java/me/lucko/luckperms/common/context/manager/QueryOptionsCache.java index 457969b70..ee92a11e8 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/QueryOptionsCache.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/manager/QueryOptionsCache.java @@ -23,13 +23,11 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context; +package me.lucko.luckperms.common.context.manager; import me.lucko.luckperms.common.cache.ExpiringCache; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.concurrent.TimeUnit; @@ -39,11 +37,11 @@ * * @param the player type */ -public final class QueryOptionsCache extends ExpiringCache implements QueryOptionsSupplier { +final class QueryOptionsCache extends ExpiringCache implements QueryOptionsSupplier { private final T subject; - private final ContextManager contextManager; + private final ContextManagerBase contextManager; - public QueryOptionsCache(T subject, ContextManager contextManager) { + QueryOptionsCache(T subject, ContextManagerBase contextManager) { super(50L, TimeUnit.MILLISECONDS); // expire roughly every tick this.subject = subject; this.contextManager = contextManager; @@ -63,4 +61,9 @@ public QueryOptions getQueryOptions() { public ImmutableContextSet getContextSet() { return get().context(); } + + @Override + public void invalidateCache() { + invalidate(); + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/context/QueryOptionsSupplier.java b/common/src/main/java/me/lucko/luckperms/common/context/manager/QueryOptionsSupplier.java similarity index 95% rename from common/src/main/java/me/lucko/luckperms/common/context/QueryOptionsSupplier.java rename to common/src/main/java/me/lucko/luckperms/common/context/manager/QueryOptionsSupplier.java index 351fb25f1..b44f92446 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/QueryOptionsSupplier.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/manager/QueryOptionsSupplier.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context; +package me.lucko.luckperms.common.context.manager; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.query.QueryOptions; @@ -39,4 +39,6 @@ default ImmutableContextSet getContextSet() { return getQueryOptions().context(); } + void invalidateCache(); + } diff --git a/common/src/main/java/me/lucko/luckperms/common/context/manager/SimpleContextManager.java b/common/src/main/java/me/lucko/luckperms/common/context/manager/SimpleContextManager.java new file mode 100644 index 000000000..42819b7d6 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/context/manager/SimpleContextManager.java @@ -0,0 +1,54 @@ +/* + * 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.context.manager; + +import com.github.benmanes.caffeine.cache.LoadingCache; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.util.CaffeineFactory; +import net.luckperms.api.query.QueryOptions; + +import java.util.concurrent.TimeUnit; + +public abstract class SimpleContextManager extends ContextManagerBase { + + private final LoadingCache contextsCache = CaffeineFactory.newBuilder() + .expireAfterWrite(50, TimeUnit.MILLISECONDS) + .build(this::calculate); + + protected SimpleContextManager(LuckPermsPlugin plugin, Class subjectClass, Class

    playerClass) { + super(plugin, subjectClass, playerClass); + } + + @Override + public QueryOptions getQueryOptions(S subject) { + return this.contextsCache.get(subject); + } + + @Override + public void invalidateCache(S subject) { + this.contextsCache.invalidate(subject); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/context/ContextSetConfigurateSerializer.java b/common/src/main/java/me/lucko/luckperms/common/context/serializer/ContextSetConfigurateSerializer.java similarity index 94% rename from common/src/main/java/me/lucko/luckperms/common/context/ContextSetConfigurateSerializer.java rename to common/src/main/java/me/lucko/luckperms/common/context/serializer/ContextSetConfigurateSerializer.java index dffaf87b5..b0ad9f184 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/ContextSetConfigurateSerializer.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/serializer/ContextSetConfigurateSerializer.java @@ -23,16 +23,13 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context; +package me.lucko.luckperms.common.context.serializer; import com.google.common.base.Preconditions; - -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; -import me.lucko.luckperms.common.context.contextset.MutableContextSetImpl; - +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.MutableContextSetImpl; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.MutableContextSet; - import ninja.leaping.configurate.ConfigurationNode; import java.util.ArrayList; diff --git a/common/src/main/java/me/lucko/luckperms/common/context/ContextSetJsonSerializer.java b/common/src/main/java/me/lucko/luckperms/common/context/serializer/ContextSetJsonSerializer.java similarity index 95% rename from common/src/main/java/me/lucko/luckperms/common/context/ContextSetJsonSerializer.java rename to common/src/main/java/me/lucko/luckperms/common/context/serializer/ContextSetJsonSerializer.java index bcee83d31..fab2178f3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/context/ContextSetJsonSerializer.java +++ b/common/src/main/java/me/lucko/luckperms/common/context/serializer/ContextSetJsonSerializer.java @@ -23,7 +23,7 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.context; +package me.lucko.luckperms.common.context.serializer; import com.google.common.base.Preconditions; import com.google.gson.Gson; @@ -31,10 +31,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; - -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; -import me.lucko.luckperms.common.context.contextset.MutableContextSetImpl; - +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.MutableContextSetImpl; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.MutableContextSet; 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 12a1cb7b7..0f76577c5 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 @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.dependencies; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.dependencies.relocation.Relocation; import me.lucko.luckperms.common.dependencies.relocation.RelocationHelper; @@ -35,6 +34,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.List; +import java.util.Locale; /** * The dependencies used by LuckPerms. @@ -44,48 +44,47 @@ public enum Dependency { ASM( "org.ow2.asm", "asm", - "9.1", - "zaTeRV+rSP8Ly3xItGOUR9TehZp6/DCglKmG8JNr66I=" + "9.8", + "h26raoPa7K1cpn65/KuwY8l7WuuM8fynqYns3hdSIFE=" ), ASM_COMMONS( "org.ow2.asm", "asm-commons", - "9.1", - "r8sm3B/BLAxKma2mcJCN2C4Y38SIyvXuklRplrRwwAw=" + "9.8", + "MwGhwctMWfzFKSZI2sHXxa7UwPBn376IhzuM3+d0BPQ=" ), JAR_RELOCATOR( "me.lucko", "jar-relocator", - "1.4", - "1RsiF3BiVztjlfTA+svDCuoDSGFuSpTZYHvUK8yBx8I=" + "1.7", + "b30RhOF6kHiHl+O5suNLh/+eAr1iOFEFLXhwkHHDu4I=" ), - ADVENTURE( "me{}lucko", "adventure-api", - "4.7.1", - "Kp0YN1he11ykhB9vSnUVHRq4/pTuOon+XuklDsSHsQw=", + "4.21.1", + "kQJlZ0gUxdTRRkskT43qiy2kpt9s654LvB0nqoCP6YE=", Relocation.of("adventure", "net{}kyori{}adventure") ), ADVENTURE_PLATFORM( "me{}lucko", "adventure-platform-api", - "4.7.0", - "CyYWxQuoN4vHte/5HuFDZEqrBGMi9Vv7uH3toJW1Z5Y=", + "4.21.1", + "Kk8IkEMVa9ITBfC3yocpcXQiZ9CwN9VxeWjKUD8I0n0=", Relocation.of("adventure", "net{}kyori{}adventure") ), ADVENTURE_PLATFORM_BUKKIT( "me{}lucko", "adventure-platform-bukkit", - "4.7.0", - "9H5MHWbJlZAHZR5zqqjW3QBU0GhJ1l9KgLGE4mKHDe8=", + "4.21.1", + "NffwBnfT/Mc6VpsmTcaXPvckv9T4vPJD83adt0C8vao=", Relocation.of("adventure", "net{}kyori{}adventure") ), ADVENTURE_PLATFORM_BUNGEECORD( "me{}lucko", "adventure-platform-bungeecord", - "4.7.0", - "puM9PtfRzhp1Gq+ZxRAhVZqDblN1P2bb8FUnhLkMVsA=", + "4.21.1", + "7hnndD6dO6LZoRbtOBdn6OFK0y/T2PqNLHxCg6zaQlo=", Relocation.of("adventure", "net{}kyori{}adventure") ), EVENT( @@ -98,15 +97,15 @@ public enum Dependency { CAFFEINE( "com{}github{}ben-manes{}caffeine", "caffeine", - "2.9.0", - "VFMotEO3XLbTHfRKfL3m36GlN72E/dzRFH9B5BJiX2o=", + "3.2.0", + "7EEd/fDAPyUhhkjOiYYWMLcWgOWFippyeOusjlXKs9c=", Relocation.of("caffeine", "com{}github{}benmanes{}caffeine") ), OKIO( "com{}squareup{}" + RelocationHelper.OKIO_STRING, RelocationHelper.OKIO_STRING, - "1.17.5", - "Gaf/SNhtPPRJf38lD78pX0MME6Uo3Vt7ID+CGAK4hq0=", + "1.17.6", + "joiwVVI8yAYT37hE1Zh0DhCtpi9L2YMEzdFAxYVMw7Y=", Relocation.of(RelocationHelper.OKIO_STRING, RelocationHelper.OKIO_STRING) ), OKHTTP( @@ -120,15 +119,15 @@ public enum Dependency { BYTEBUDDY( "net{}bytebuddy", "byte-buddy", - "1.10.22", - "+TGtxDkxd6+lJExHJXqDlV4n/gR8QJN4xu2gkPsHSoQ=", + "1.15.11", + "+giZiq4ee9roO94HEsUOhETXHA4MGWuyJHrejUrQ65A=", Relocation.of("bytebuddy", "net{}bytebuddy") ), COMMODORE( "me{}lucko", "commodore", - "1.9", - "EhaLLqbgPnVYa61RumUc7l3r6wcGg2edDh2+PR8pvHI=", + "2.2", + "hmZ3A/Sf8LvrT95buTlFNwdEBZ36X9Ks8SKOS1b7f28=", Relocation.of("commodore", "me{}lucko{}commodore") ), COMMODORE_FILE( @@ -141,25 +140,25 @@ public enum Dependency { MARIADB_DRIVER( "org{}mariadb{}jdbc", "mariadb-java-client", - "2.7.2", - "o/Z3bfCELPZefxWFFQEtUwfalJ9mBCKC4e5EdN0Z9Eg=", + "3.5.2", + "8vPDwaO9rKad0dThzYrtB1JC/HKuQUY924LjZ7OI9q0=", Relocation.of("mariadb", "org{}mariadb{}jdbc") ), MYSQL_DRIVER( - "mysql", - "mysql-connector-java", - "8.0.23", - "/31bQCr9OcEnh0cVBaM6MEEDsjjsG3pE6JNtMynadTU=", + "com{}mysql", + "mysql-connector-j", + "9.3.0", + "bI5mkrUhN22JvFYYwWzer4xhhUMp9PolZ37Qh3bFu3Y=", Relocation.of("mysql", "com{}mysql") ), POSTGRESQL_DRIVER( "org{}postgresql", "postgresql", - "42.2.19", - "IydH+gkk2Iom36QrgSi2+hFAgC2AQSWJFZboyl8pEyI=", + "42.7.6", + "8qHMA1LdXlxvZdut/ye+4Awy5DLGrQMNB0R/ilmDxCo=", Relocation.of("postgresql", "org{}postgresql") ), - H2_DRIVER( + H2_DRIVER_LEGACY( "com.h2database", "h2", // seems to be a compat bug in 1.4.200 with older dbs @@ -169,96 +168,135 @@ public enum Dependency { // we don't apply relocations to h2 - it gets loaded via // an isolated classloader ), + H2_DRIVER( + "com.h2database", + "h2", + "2.1.214", + "1iPNwPYdIYz1SajQnxw5H/kQlhFrIuJHVHX85PvnK9A=" + // we don't apply relocations to h2 - it gets loaded via + // an isolated classloader + ), SQLITE_DRIVER( "org.xerial", "sqlite-jdbc", - "3.28.0", - "k3hOVtv1RiXgbJks+D9w6cG93Vxq0dPwEwjIex2WG2A=" + "3.49.1.0", + "XIYJ0so0HeuMb3F3iXS1ukmVx9MtfHyJ2TkqPnLDkpE=" // we don't apply relocations to sqlite - it gets loaded via // an isolated classloader ), HIKARI( "com{}zaxxer", "HikariCP", - "4.0.3", - "fAJK7/HBBjV210RTUT+d5kR9jmJNF/jifzCi6XaIxsk=", + "6.3.0", + "B8Y0QFmvMKE1FEIJx8i9ZmuIIxJEIuyFmGTSCdSrfKE=", Relocation.of("hikari", "com{}zaxxer{}hikari") ), SLF4J_SIMPLE( "org.slf4j", "slf4j-simple", - "1.7.30", - "i5J5y/9rn4hZTvrjzwIDm2mVAw7sAj7UOSh0jEFnD+4=" + "1.7.36", + "Lzm+2UPWJN+o9BAtBXEoOhCHC2qjbxl6ilBvFHAQwQ8=" ), SLF4J_API( "org.slf4j", "slf4j-api", - "1.7.30", - "zboHlk0btAoHYUhcax6ML4/Z6x0ZxTkorA1/lRAQXFc=" + "1.7.36", + "0+9XXj5JeWeNwBvx3M5RAhSTtNEft/G+itmCh3wWocA=" ), - MONGODB_DRIVER( + MONGODB_DRIVER_CORE( "org.mongodb", - "mongo-java-driver", - "3.12.8", - "92uqr4qaL3dbw5wrb8sQWQqFxpzr/Y/DhForeyg3taI=", + "mongodb-driver-core", + "5.5.0", + "69tQuKep52lbYvX2YM+J6GGlYkNySXkMBDuk6BqtsJE=", + Relocation.of("mongodb", "com{}mongodb"), + Relocation.of("bson", "org{}bson") + ), + MONGODB_DRIVER_LEGACY( + "org.mongodb", + "mongodb-driver-legacy", + "5.5.0", + "yo/0wEdLw0/Md1xqgEd/iqiKV+t0AqAcdnS1TNAaygM=", + Relocation.of("mongodb", "com{}mongodb"), + Relocation.of("bson", "org{}bson") + ), + MONGODB_DRIVER_SYNC( + "org.mongodb", + "mongodb-driver-sync", + "5.5.0", + "nFECiREXgMc5Ikamvmnzaxumhz75NKG+ajIhAW/ioPI=", + Relocation.of("mongodb", "com{}mongodb"), + Relocation.of("bson", "org{}bson") + ), + MONGODB_DRIVER_BSON( + "org.mongodb", + "bson", + "5.5.0", + "hQx5w0v/DuQvASpnGXkLuWxkhXhewDTTAmrifPWbBJQ=", Relocation.of("mongodb", "com{}mongodb"), Relocation.of("bson", "org{}bson") ), JEDIS( "redis.clients", "jedis", - "3.5.2", - "jX3340YaYjHFQN2sA+GCo33LB4FuIYKgQUPUv2MK/Xo=", + "5.2.0", + "3U+9osED8xmrSVrbK8GQYTmEB0bP1MZrJ3ENGvmDgtQ=", Relocation.of("jedis", "redis{}clients{}jedis"), Relocation.of("commonspool2", "org{}apache{}commons{}pool2") ), + NATS( + "io.nats", + "jnats", + "2.21.1", + "QHUHUCnCCy/oRSwoqhy0245SrvD4lwCfc+ZVmemHXLg=", + Relocation.of("nats", "io{}nats{}client") + ), RABBITMQ( "com{}rabbitmq", "amqp-client", - "5.12.0", - "CxliwVWAnPKi5BwxCu1S1SGzx5fbhTk5JCKdBS27P2c=", + "5.25.0", + "WqlvAFCEE56xB32UtV3GQo7KfafizFPqtEp3M5H4qo8=", Relocation.of("rabbitmq", "com{}rabbitmq") ), COMMONS_POOL_2( "org.apache.commons", "commons-pool2", - "2.9.0", - "vJGbQmv6+zHsxF1mUqnxN0YkZdhJ+zhz142Qw/jTWwE=", + "2.12.1", + "UnPIvIwNyiIRF1wNJ++9cijvrplomqwAGo4e+Ohy6e8=", Relocation.of("commonspool2", "org{}apache{}commons{}pool2") ), CONFIGURATE_CORE( "org{}spongepowered", "configurate-core", - "3.7.2", - "XF2LzWLkSV0wyQRDt33I+gDlf3t2WzxH1h8JCZZgPp4=", + "3.7.3", + "06R3WDViB84WtSkHTudV8TSPxF1eQyCyfab8L7Pvo2M=", Relocation.of("configurate", "ninja{}leaping{}configurate") ), CONFIGURATE_GSON( "org{}spongepowered", "configurate-gson", - "3.7.2", - "9S/mp3Ig9De7NNd6+2kX+L4R90bHnAosSNVbFjrl7sM=", + "3.7.3", + "QM+bGrgrzfwT9nvIvTHtR2TUEpun+RwlXIO/a9BU0Mc=", Relocation.of("configurate", "ninja{}leaping{}configurate") ), CONFIGURATE_YAML( "org{}spongepowered", "configurate-yaml", - "3.7.2", - "OBfYn4nSMGZfVf2DoZhZq+G9TF1mODX/C5OOz/mkPmc=", + "3.7.3", + "a04vRkLhigIqiG/gdVvK7c1YiBQJ7k1q/kBNsS9OVDs=", Relocation.of("configurate", "ninja{}leaping{}configurate") ), SNAKEYAML( "org.yaml", "snakeyaml", - "1.28", - "NURqFCFDXUXkxqwN47U3hSfVzCRGwHGD4kRHcwzh//o=", + "1.33", + "Ef9Fl4jwoteB9WpKhtfmkgLOus0Cc9UmnErp8C8/2PA=", Relocation.of("yaml", "org{}yaml{}snakeyaml") ), CONFIGURATE_HOCON( "org{}spongepowered", "configurate-hocon", - "3.7.2", - "GOORZlK1FKLzdIm7dKyyXtBdvk7Z89HARAd2H6NiWSY=", + "3.7.3", + "e/UDpbIrWdJNB6yMFXtrOnnNn3nptmSv/J8n46uQPNs=", Relocation.of("configurate", "ninja{}leaping{}configurate"), Relocation.of("hocon", "com{}typesafe{}config") ), @@ -313,8 +351,13 @@ private static String rewriteEscaping(String s) { return s.replace("{}", "."); } - public String getFileName() { - return name().toLowerCase().replace('_', '-') + "-" + this.version; + public String getFileName(String classifier) { + String name = name().toLowerCase(Locale.ROOT).replace('_', '-'); + String extra = classifier == null || classifier.isEmpty() + ? "" + : "-" + classifier; + + return name + "-" + this.version + extra + ".jar"; } String getMavenRepoPath() { diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java index 7cbee16a8..3ee7b502a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManager.java @@ -25,195 +25,40 @@ package me.lucko.luckperms.common.dependencies; -import com.google.common.collect.ImmutableSet; - -import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; -import me.lucko.luckperms.common.dependencies.relocation.Relocation; -import me.lucko.luckperms.common.dependencies.relocation.RelocationHandler; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.StorageType; -import me.lucko.luckperms.common.util.MoreFiles; - -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.Set; -import java.util.concurrent.CountDownLatch; /** * Loads and manages runtime dependencies for the plugin. */ -public class DependencyManager { - - /** The plugin instance */ - private final LuckPermsPlugin plugin; - /** A registry containing plugin specific behaviour for dependencies. */ - private final DependencyRegistry registry; - /** The path where library jars are cached. */ - private final Path cacheDirectory; - - /** A map of dependencies which have already been loaded. */ - private final EnumMap loaded = new EnumMap<>(Dependency.class); - /** A map of isolated classloaders which have been created. */ - private final Map, IsolatedClassLoader> loaders = new HashMap<>(); - /** Cached relocation handler instance. */ - private @MonotonicNonNull RelocationHandler relocationHandler = null; - - public DependencyManager(LuckPermsPlugin plugin) { - this.plugin = plugin; - this.registry = new DependencyRegistry(plugin); - this.cacheDirectory = setupCacheDirectory(plugin); - } - - private synchronized RelocationHandler getRelocationHandler() { - if (this.relocationHandler == null) { - this.relocationHandler = new RelocationHandler(this); - } - return this.relocationHandler; - } - - public IsolatedClassLoader obtainClassLoaderWith(Set dependencies) { - ImmutableSet set = ImmutableSet.copyOf(dependencies); - - for (Dependency dependency : dependencies) { - if (!this.loaded.containsKey(dependency)) { - throw new IllegalStateException("Dependency " + dependency + " is not loaded."); - } - } - - synchronized (this.loaders) { - IsolatedClassLoader classLoader = this.loaders.get(set); - if (classLoader != null) { - return classLoader; - } - - URL[] urls = set.stream() - .map(this.loaded::get) - .map(file -> { - try { - return file.toUri().toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - }) - .toArray(URL[]::new); - - classLoader = new IsolatedClassLoader(urls); - this.loaders.put(set, classLoader); - return classLoader; - } - } - - public void loadStorageDependencies(Set storageTypes) { - loadDependencies(this.registry.resolveStorageDependencies(storageTypes)); - } - - public void loadDependencies(Set dependencies) { - CountDownLatch latch = new CountDownLatch(dependencies.size()); - - for (Dependency dependency : dependencies) { - this.plugin.getBootstrap().getScheduler().async().execute(() -> { - try { - loadDependency(dependency); - } catch (Throwable e) { - this.plugin.getLogger().severe("Unable to load dependency " + dependency.name() + ".", e); - } finally { - latch.countDown(); - } - }); - } - - try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - private void loadDependency(Dependency dependency) throws Exception { - if (this.loaded.containsKey(dependency)) { - return; - } - - Path file = remapDependency(dependency, downloadDependency(dependency)); - - this.loaded.put(dependency, file); - - if (this.registry.shouldAutoLoad(dependency)) { - this.plugin.getBootstrap().getClassPathAppender().addJarToClasspath(file); - } - } - - private Path downloadDependency(Dependency dependency) throws DependencyDownloadException { - Path file = this.cacheDirectory.resolve(dependency.getFileName() + ".jar"); - - // if the file already exists, don't attempt to re-download it. - if (Files.exists(file)) { - return file; - } - - DependencyDownloadException lastError = null; - - // attempt to download the dependency from each repo in order. - for (DependencyRepository repo : DependencyRepository.values()) { - try { - repo.download(dependency, file); - return file; - } catch (DependencyDownloadException e) { - lastError = e; - } - } - - throw Objects.requireNonNull(lastError); - } - - private Path remapDependency(Dependency dependency, Path normalFile) throws Exception { - List rules = new ArrayList<>(dependency.getRelocations()); - this.registry.applyRelocationSettings(dependency, rules); - - if (rules.isEmpty()) { - return normalFile; - } - - Path remappedFile = this.cacheDirectory.resolve(dependency.getFileName() + "-remapped.jar"); - - // if the remapped source exists already, just use that. - if (Files.exists(remappedFile)) { - return remappedFile; - } - - getRelocationHandler().remap(normalFile, remappedFile, rules); - return remappedFile; - } - - private static Path setupCacheDirectory(LuckPermsPlugin plugin) { - Path cacheDirectory = plugin.getBootstrap().getDataDirectory().resolve("libs"); - try { - MoreFiles.createDirectoriesIfNotExists(cacheDirectory); - } catch (IOException e) { - throw new RuntimeException("Unable to create libs directory", e); - } - - Path oldCacheDirectory = plugin.getBootstrap().getDataDirectory().resolve("lib"); - if (Files.exists(oldCacheDirectory)) { - try { - MoreFiles.deleteDirectory(oldCacheDirectory); - } catch (IOException e) { - plugin.getLogger().warn("Unable to delete lib directory", e); - } - } - - return cacheDirectory; - } - +public interface DependencyManager extends AutoCloseable { + + /** + * Loads dependencies. + * + * @param dependencies the dependencies to load + */ + void loadDependencies(Set dependencies); + + /** + * Loads storage dependencies. + * + * @param storageTypes the storage types in use + * @param redis if redis is being used + * @param rabbitmq if rabbitmq is being used + * @param nats if nats is being used + */ + void loadStorageDependencies(Set storageTypes, boolean redis, boolean rabbitmq, boolean nats); + + /** + * Obtains an isolated classloader containing the given dependencies. + * + * @param dependencies the dependencies + * @return the classloader + */ + ClassLoader obtainClassLoaderWith(Set dependencies); + + @Override + void close(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManagerImpl.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManagerImpl.java new file mode 100644 index 000000000..f5b150f3a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyManagerImpl.java @@ -0,0 +1,266 @@ +/* + * 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.dependencies; + +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; +import me.lucko.luckperms.common.dependencies.relocation.Relocation; +import me.lucko.luckperms.common.dependencies.relocation.RelocationHandler; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.storage.StorageType; +import me.lucko.luckperms.common.util.MoreFiles; +import net.luckperms.api.platform.Platform; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +/** + * Loads and manages runtime dependencies for the plugin. + */ +public class DependencyManagerImpl implements DependencyManager { + + /** A registry containing plugin specific behaviour for dependencies. */ + private final DependencyRegistry registry; + /** The path where library jars are cached. */ + private final Path cacheDirectory; + /** The classpath appender to preload dependencies into */ + private final ClassPathAppender classPathAppender; + /** The executor to use when loading dependencies */ + private final Executor loadingExecutor; + /** A collection of repositories to attempt to download dependencies from. */ + private final Collection repositories; + + /** A map of dependencies which have already been loaded. */ + private final EnumMap loaded = new EnumMap<>(Dependency.class); + /** A map of isolated classloaders which have been created. */ + private final Map, IsolatedClassLoader> loaders = new HashMap<>(); + /** Cached relocation handler instance. */ + private @MonotonicNonNull RelocationHandler relocationHandler = null; + + public DependencyManagerImpl(LuckPermsPlugin plugin, Collection repositories) { + this.registry = new DependencyRegistry(plugin.getBootstrap().getType()); + this.cacheDirectory = setupCacheDirectory(plugin); + this.classPathAppender = plugin.getBootstrap().getClassPathAppender(); + this.loadingExecutor = plugin.getBootstrap().getScheduler().async(); + this.repositories = repositories; + } + + public DependencyManagerImpl(Path cacheDirectory, Executor executor) { // standalone pre-loader + this.registry = new DependencyRegistry(Platform.Type.STANDALONE); + this.cacheDirectory = cacheDirectory; + this.classPathAppender = null; + this.loadingExecutor = executor; + this.repositories = DependencyRepository.REMOTE_MAVEN_REPOSITORIES; + } + + private synchronized RelocationHandler getRelocationHandler() { + if (this.relocationHandler == null) { + this.relocationHandler = new RelocationHandler(this); + } + return this.relocationHandler; + } + + @Override + public ClassLoader obtainClassLoaderWith(Set dependencies) { + ImmutableSet set = ImmutableSet.copyOf(dependencies); + + for (Dependency dependency : dependencies) { + if (!this.loaded.containsKey(dependency)) { + throw new IllegalStateException("Dependency " + dependency + " is not loaded."); + } + } + + synchronized (this.loaders) { + IsolatedClassLoader classLoader = this.loaders.get(set); + if (classLoader != null) { + return classLoader; + } + + URL[] urls = set.stream() + .map(this.loaded::get) + .map(file -> { + try { + return file.toUri().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }) + .toArray(URL[]::new); + + classLoader = new IsolatedClassLoader(urls); + this.loaders.put(set, classLoader); + return classLoader; + } + } + + @Override + public void loadStorageDependencies(Set storageTypes, boolean redis, boolean rabbitmq, boolean nats) { + loadDependencies(this.registry.resolveStorageDependencies(storageTypes, redis, rabbitmq, nats)); + } + + @Override + public void loadDependencies(Set dependencies) { + CountDownLatch latch = new CountDownLatch(dependencies.size()); + + for (Dependency dependency : dependencies) { + if (this.loaded.containsKey(dependency)) { + latch.countDown(); + continue; + } + + this.loadingExecutor.execute(() -> { + try { + loadDependency(dependency); + } catch (Throwable e) { + new RuntimeException("Unable to load dependency " + dependency.name(), e).printStackTrace(); + } finally { + latch.countDown(); + } + }); + } + + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void loadDependency(Dependency dependency) throws Exception { + if (this.loaded.containsKey(dependency)) { + return; + } + + Path file = remapDependency(dependency, downloadDependency(dependency)); + + this.loaded.put(dependency, file); + + if (this.classPathAppender != null && this.registry.shouldAutoLoad(dependency)) { + this.classPathAppender.addJarToClasspath(file); + } + } + + private Path downloadDependency(Dependency dependency) throws DependencyDownloadException { + Path file = this.cacheDirectory.resolve(dependency.getFileName(null)); + + // if the file already exists, don't attempt to re-download it. + if (Files.exists(file)) { + return file; + } + + DependencyDownloadException lastError = null; + + // attempt to download the dependency from each repo in order. + for (DependencyRepository repo : this.repositories) { + try { + repo.download(dependency, file); + return file; + } catch (DependencyDownloadException e) { + lastError = e; + } + } + + throw Objects.requireNonNull(lastError); + } + + private Path remapDependency(Dependency dependency, Path normalFile) throws Exception { + List rules = new ArrayList<>(dependency.getRelocations()); + this.registry.applyRelocationSettings(dependency, rules); + + if (rules.isEmpty()) { + return normalFile; + } + + // subtly different file name if we're relocating gson - allows users to switch jars seamlessly + String remappedFileName = dependency.getFileName(DependencyRegistry.isGsonRelocated() ? "remap" : "remapped"); + Path remappedFile = this.cacheDirectory.resolve(remappedFileName); + + // if the remapped source exists already, just use that. + if (Files.exists(remappedFile)) { + return remappedFile; + } + + getRelocationHandler().remap(normalFile, remappedFile, rules); + return remappedFile; + } + + private static Path setupCacheDirectory(LuckPermsPlugin plugin) { + Path cacheDirectory = plugin.getBootstrap().getDataDirectory().resolve("libs"); + try { + MoreFiles.createDirectoriesIfNotExists(cacheDirectory); + } catch (IOException e) { + throw new RuntimeException("Unable to create libs directory", e); + } + + Path oldCacheDirectory = plugin.getBootstrap().getDataDirectory().resolve("lib"); + if (Files.exists(oldCacheDirectory)) { + try { + MoreFiles.deleteDirectory(oldCacheDirectory); + } catch (IOException e) { + plugin.getLogger().warn("Unable to delete lib directory", e); + } + } + + return cacheDirectory; + } + + @Override + public void close() { + IOException firstEx = null; + + for (IsolatedClassLoader loader : this.loaders.values()) { + try { + loader.close(); + } catch (IOException ex) { + if (firstEx == null) { + firstEx = ex; + } else { + firstEx.addSuppressed(ex); + } + } + } + + if (firstEx != null) { + firstEx.printStackTrace(); + } + } + +} 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 3f6698b30..088b25caa 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 @@ -25,16 +25,13 @@ package me.lucko.luckperms.common.dependencies; -import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.SetMultimap; import com.google.gson.JsonElement; - -import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.dependencies.relocation.Relocation; import me.lucko.luckperms.common.dependencies.relocation.RelocationHandler; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.StorageType; - import net.luckperms.api.platform.Platform; import java.util.LinkedHashSet; @@ -46,43 +43,51 @@ */ public class DependencyRegistry { - private static final ListMultimap STORAGE_DEPENDENCIES = ImmutableListMultimap.builder() - .putAll(StorageType.YAML, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_YAML) - .putAll(StorageType.JSON, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_GSON) - .putAll(StorageType.HOCON, Dependency.HOCON_CONFIG, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_HOCON) - .putAll(StorageType.TOML, Dependency.TOML4J, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_TOML) - .putAll(StorageType.YAML_COMBINED, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_YAML) - .putAll(StorageType.JSON_COMBINED, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_GSON) - .putAll(StorageType.HOCON_COMBINED, Dependency.HOCON_CONFIG, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_HOCON) - .putAll(StorageType.TOML_COMBINED, Dependency.TOML4J, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_TOML) - .putAll(StorageType.MONGODB, Dependency.MONGODB_DRIVER) - .putAll(StorageType.MARIADB, Dependency.MARIADB_DRIVER, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE, Dependency.HIKARI) - .putAll(StorageType.MYSQL, Dependency.MYSQL_DRIVER, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE, Dependency.HIKARI) - .putAll(StorageType.POSTGRESQL, Dependency.POSTGRESQL_DRIVER, Dependency.SLF4J_API, Dependency.SLF4J_SIMPLE, Dependency.HIKARI) - .putAll(StorageType.SQLITE, Dependency.SQLITE_DRIVER) - .putAll(StorageType.H2, Dependency.H2_DRIVER) + private static final SetMultimap STORAGE_DEPENDENCIES = ImmutableSetMultimap.builder() + .putAll(StorageType.YAML, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_YAML, Dependency.SNAKEYAML) + .putAll(StorageType.YAML_COMBINED, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_YAML, Dependency.SNAKEYAML) + .putAll(StorageType.JSON, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_GSON) + .putAll(StorageType.JSON_COMBINED, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_GSON) + .putAll(StorageType.HOCON, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_HOCON, Dependency.HOCON_CONFIG) + .putAll(StorageType.HOCON_COMBINED, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_HOCON, Dependency.HOCON_CONFIG) + .putAll(StorageType.TOML, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_TOML, Dependency.TOML4J) + .putAll(StorageType.TOML_COMBINED, Dependency.CONFIGURATE_CORE, Dependency.CONFIGURATE_TOML, Dependency.TOML4J) + .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) + .putAll(StorageType.SQLITE, Dependency.SQLITE_DRIVER) + .putAll(StorageType.H2, Dependency.H2_DRIVER) .build(); - private final LuckPermsPlugin plugin; + private static final Set SNAKEYAML_PROVIDED_BY_PLATFORM = ImmutableSet.of( + Platform.Type.BUKKIT, Platform.Type.BUNGEECORD, Platform.Type.SPONGE, Platform.Type.NUKKIT + ); + + private final Platform.Type platformType; - public DependencyRegistry(LuckPermsPlugin plugin) { - this.plugin = plugin; + public DependencyRegistry(Platform.Type platformType) { + this.platformType = platformType; } - public Set resolveStorageDependencies(Set storageTypes) { + public Set resolveStorageDependencies(Set storageTypes, boolean redis, boolean rabbitmq, boolean nats) { Set dependencies = new LinkedHashSet<>(); for (StorageType storageType : storageTypes) { dependencies.addAll(STORAGE_DEPENDENCIES.get(storageType)); } - if (this.plugin.getConfiguration().get(ConfigKeys.REDIS_ENABLED)) { + if (redis) { dependencies.add(Dependency.COMMONS_POOL_2); dependencies.add(Dependency.JEDIS); dependencies.add(Dependency.SLF4J_API); dependencies.add(Dependency.SLF4J_SIMPLE); } - if (this.plugin.getConfiguration().get(ConfigKeys.RABBITMQ_ENABLED)) { + if (nats) { + dependencies.add(Dependency.NATS); + } + + if (rabbitmq) { dependencies.add(Dependency.RABBITMQ); } @@ -92,20 +97,23 @@ public Set resolveStorageDependencies(Set storageTypes) dependencies.remove(Dependency.SLF4J_SIMPLE); } + // don't load snakeyaml if it's provided by the platform + if (dependencies.contains(Dependency.SNAKEYAML) && SNAKEYAML_PROVIDED_BY_PLATFORM.contains(this.platformType)) { + dependencies.remove(Dependency.SNAKEYAML); + } + return dependencies; } public void applyRelocationSettings(Dependency dependency, List relocations) { - Platform.Type type = this.plugin.getBootstrap().getType(); - // support for LuckPerms legacy (bukkit 1.7.10) - if (!RelocationHandler.DEPENDENCIES.contains(dependency) && JsonElement.class.getName().startsWith("me.lucko")) { + if (!RelocationHandler.DEPENDENCIES.contains(dependency) && isGsonRelocated()) { relocations.add(Relocation.of("guava", "com{}google{}common")); relocations.add(Relocation.of("gson", "com{}google{}gson")); } - // relocate yaml within configurate when running velocity - if (dependency == Dependency.CONFIGURATE_YAML && type == Platform.Type.VELOCITY) { + // relocate yaml within configurate if its being provided by LP + if (dependency == Dependency.CONFIGURATE_YAML && !SNAKEYAML_PROVIDED_BY_PLATFORM.contains(this.platformType)) { relocations.add(Relocation.of("yaml", "org{}yaml{}snakeyaml")); } } @@ -118,6 +126,7 @@ public boolean shouldAutoLoad(Dependency dependency) { case ASM_COMMONS: case JAR_RELOCATOR: case H2_DRIVER: + case H2_DRIVER_LEGACY: case SQLITE_DRIVER: return false; default: @@ -125,17 +134,27 @@ public boolean shouldAutoLoad(Dependency dependency) { } } + private boolean slf4jPresent() { + if (this.platformType == Platform.Type.HYTALE) { + // always load SLf4J on hytale, as it's not provided by the platform, and we don't want to + // accidentally use a version shaded in a different plugin + return false; + } + return classExists("org.slf4j.Logger") && classExists("org.slf4j.LoggerFactory"); + } + + @SuppressWarnings("ConstantConditions") + public static boolean isGsonRelocated() { + return JsonElement.class.getName().startsWith("me.lucko"); + } + private static boolean classExists(String className) { try { Class.forName(className); return true; - } catch (ClassNotFoundException e) { + } catch (Exception e) { return false; } } - private static boolean slf4jPresent() { - return classExists("org.slf4j.Logger") && classExists("org.slf4j.LoggerFactory"); - } - } diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRepository.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRepository.java index 3606a2906..030b953cc 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRepository.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/DependencyRepository.java @@ -34,6 +34,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Base64; +import java.util.Collection; +import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -42,18 +44,26 @@ public enum DependencyRepository { /** - * Maven Central mirror repository. + * A {@link DependencyRepository} that downloads from the + * LuckPerms Maven Central mirror repository. * - *

    This is used to reduce the load on repo.maven.org - I'm told they - * don't like being used as a CDN.

    + *

    This is used to reduce the load on repo.maven.org.

    + * + *

    Although Maven Central is technically a CDN, it is meant for developer use, + * not end-user products. It is trivial and not very expensive for us to provide a + * mirror, which will absorb any traffic caused by LP.

    + * + *

    LuckPerms will fallback to the real-thing if the mirror ever goes offline. + * Retrieved content is validated with a checksum, so there is no risk to integrity.

    */ - // Please ask me (@lucko) before using this mirror in your own project. - LUCK_MIRROR("https://nexus.lucko.me/repository/maven-central/") { + MAVEN_CENTRAL_MIRROR { + private static final String URL = "https://libraries.luckperms.net/"; + @Override - protected URLConnection openConnection(Dependency dependency) throws IOException { - URLConnection connection = super.openConnection(dependency); + protected InputStream openStream(Dependency dependency) throws IOException { + URL dependencyUrl = new URL(URL + dependency.getMavenRepoPath()); - // Tell nexus who we are + URLConnection connection = dependencyUrl.openConnection(); connection.setRequestProperty("User-Agent", "luckperms"); // Set a connect/read timeout, so if the mirror goes offline we can fallback @@ -61,32 +71,46 @@ protected URLConnection openConnection(Dependency dependency) throws IOException connection.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(5)); connection.setReadTimeout((int) TimeUnit.SECONDS.toMillis(10)); - return connection; + return connection.getInputStream(); } }, /** - * Maven Central. + * A {@link DependencyRepository} that downloads from the official Maven Central mirror repository. */ - MAVEN_CENTRAL("https://repo1.maven.org/maven2/"); + MAVEN_CENTRAL { + private static final String URL = "https://repo1.maven.org/maven2/"; - private final String url; + @Override + protected InputStream openStream(Dependency dependency) throws IOException { + URL dependencyUrl = new URL(URL + dependency.getMavenRepoPath()); + return dependencyUrl.openStream(); + } + }, - DependencyRepository(String url) { - this.url = url; - } + /** + * A {@link DependencyRepository} that searches for dependencies bundled inside the jar (jar-in-jar). + */ + JAR_IN_JAR { + private static final String PATH = "luckperms/deps/"; + + @Override + protected synchronized InputStream openStream(Dependency dependency) throws IOException { + String path = PATH + dependency.getFileName(null) + "injar"; // extension becomes .jarinjar + return getClass().getClassLoader().getResourceAsStream(path); + } + }; + + public static final Collection REMOTE_MAVEN_REPOSITORIES = List.of(MAVEN_CENTRAL_MIRROR, MAVEN_CENTRAL); /** - * Opens a connection to the given {@code dependency}. + * Opens an InputStream to the given {@code dependency}. * * @param dependency the dependency to download - * @return the connection - * @throws IOException if unable to open a connection + * @return the input stream + * @throws IOException if unable to open an input stream */ - protected URLConnection openConnection(Dependency dependency) throws IOException { - URL dependencyUrl = new URL(this.url + dependency.getMavenRepoPath()); - return dependencyUrl.openConnection(); - } + protected abstract InputStream openStream(Dependency dependency) throws IOException; /** * Downloads the raw bytes of the {@code dependency}. @@ -97,8 +121,11 @@ protected URLConnection openConnection(Dependency dependency) throws IOException */ public byte[] downloadRaw(Dependency dependency) throws DependencyDownloadException { try { - URLConnection connection = openConnection(dependency); - try (InputStream in = connection.getInputStream()) { + try (InputStream in = openStream(dependency)) { + if (in == null) { + throw new DependencyDownloadException("Repository " + this.name() + " returned null stream for dependency " + dependency); + } + byte[] bytes = ByteStreams.toByteArray(in); if (bytes.length == 0) { throw new DependencyDownloadException("Empty stream"); @@ -135,7 +162,7 @@ public byte[] download(Dependency dependency) throws DependencyDownloadException } /** - * Downloads the the {@code dependency} to the {@code file}, ensuring the + * Downloads the {@code dependency} to the {@code file}, ensuring the * downloaded bytes match the checksum. * * @param dependency the dependency to download diff --git a/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java b/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java index 7ac1fb58b..29a833e85 100644 --- a/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java +++ b/common/src/main/java/me/lucko/luckperms/common/dependencies/relocation/RelocationHandler.java @@ -30,6 +30,7 @@ import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import java.io.File; +import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.nio.file.Path; @@ -51,11 +52,12 @@ public class RelocationHandler { private final Method jarRelocatorRunMethod; public RelocationHandler(DependencyManager dependencyManager) { + ClassLoader classLoader = null; try { // download the required dependencies for remapping dependencyManager.loadDependencies(DEPENDENCIES); // get a classloader containing the required dependencies as sources - IsolatedClassLoader classLoader = dependencyManager.obtainClassLoaderWith(DEPENDENCIES); + classLoader = dependencyManager.obtainClassLoaderWith(DEPENDENCIES); // load the relocator class Class jarRelocatorClass = classLoader.loadClass(JAR_RELOCATOR_CLASS); @@ -67,6 +69,14 @@ public RelocationHandler(DependencyManager dependencyManager) { this.jarRelocatorRunMethod = jarRelocatorClass.getDeclaredMethod(JAR_RELOCATOR_RUN_METHOD); this.jarRelocatorRunMethod.setAccessible(true); } catch (Exception e) { + try { + if (classLoader instanceof IsolatedClassLoader) { + ((IsolatedClassLoader) classLoader).close(); + } + } catch (IOException ex) { + e.addSuppressed(ex); + } + throw new RuntimeException(e); } } @@ -81,4 +91,5 @@ public void remap(Path input, Path output, List relocations) throws Object relocator = this.jarRelocatorConstructor.newInstance(input.toFile(), output.toFile(), mappings); this.jarRelocatorRunMethod.invoke(relocator); } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/event/AbstractEventBus.java b/common/src/main/java/me/lucko/luckperms/common/event/AbstractEventBus.java index 08f22ed98..ff11bcd96 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/AbstractEventBus.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/AbstractEventBus.java @@ -27,13 +27,11 @@ import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.kyori.event.EventSubscriber; import net.kyori.event.SimpleEventBus; import net.luckperms.api.event.EventBus; import net.luckperms.api.event.EventSubscription; import net.luckperms.api.event.LuckPermsEvent; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; diff --git a/common/src/main/java/me/lucko/luckperms/common/event/EventDispatcher.java b/common/src/main/java/me/lucko/luckperms/common/event/EventDispatcher.java index 272794236..4a7b9387c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/EventDispatcher.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/EventDispatcher.java @@ -27,7 +27,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; - import me.lucko.luckperms.common.api.implementation.ApiPermissionHolder; import me.lucko.luckperms.common.cacheddata.GroupCachedDataManager; import me.lucko.luckperms.common.cacheddata.UserCachedDataManager; @@ -40,9 +39,8 @@ import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; -import me.lucko.luckperms.common.model.nodemap.MutateResult; import me.lucko.luckperms.common.sender.Sender; - +import me.lucko.luckperms.common.util.Difference; import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.LuckPermsEvent; import net.luckperms.api.event.cause.CreationCause; @@ -60,6 +58,7 @@ import net.luckperms.api.event.log.LogNotifyEvent; import net.luckperms.api.event.log.LogPublishEvent; import net.luckperms.api.event.log.LogReceiveEvent; +import net.luckperms.api.event.messaging.CustomMessageReceiveEvent; import net.luckperms.api.event.node.NodeAddEvent; import net.luckperms.api.event.node.NodeClearEvent; import net.luckperms.api.event.node.NodeMutateEvent; @@ -72,9 +71,11 @@ import net.luckperms.api.event.player.lookup.UsernameValidityCheckEvent; import net.luckperms.api.event.source.Source; import net.luckperms.api.event.sync.ConfigReloadEvent; +import net.luckperms.api.event.sync.PostNetworkSyncEvent; import net.luckperms.api.event.sync.PostSyncEvent; import net.luckperms.api.event.sync.PreNetworkSyncEvent; import net.luckperms.api.event.sync.PreSyncEvent; +import net.luckperms.api.event.sync.SyncType; import net.luckperms.api.event.track.TrackCreateEvent; import net.luckperms.api.event.track.TrackDeleteEvent; import net.luckperms.api.event.track.TrackLoadAllEvent; @@ -95,7 +96,6 @@ import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; - import org.checkerframework.checker.nullness.qual.Nullable; import java.util.List; @@ -115,13 +115,23 @@ public AbstractEventBus getEventBus() { return this.eventBus; } - private void postAsync(Class eventClass, Object... params) { - // check against common mistakes - events with any sort of result shouldn't be posted async - if (Cancellable.class.isAssignableFrom(eventClass)) { - throw new RuntimeException("Cancellable event cannot be posted async (" + eventClass + ")"); + private LuckPermsEvent generate(Class eventClass, Object... params) { + try { + return GeneratedEventClass.generate(eventClass).newInstance(this.eventBus.getApiProvider(), params); + } catch (Throwable e) { + throw new RuntimeException("Exception occurred whilst generating event instance", e); } - if (ResultEvent.class.isAssignableFrom(eventClass)) { - throw new RuntimeException("ResultEvent event cannot be posted async (" + eventClass + ")"); + } + + private void post(Class eventClass, Object... params) { + LuckPermsEvent event = generate(eventClass, params); + this.eventBus.post(event); + } + + private void postAsync(Class eventClass, Object... params) { + // check against common mistakes - events with any sort of result shouldn't be posted async + if (Cancellable.class.isAssignableFrom(eventClass) || ResultEvent.class.isAssignableFrom(eventClass)) { + throw new RuntimeException("Event cannot be posted async (" + eventClass.getName() + ")"); } // if there aren't any handlers registered for the event, don't bother trying to post it @@ -130,24 +140,24 @@ private void postAsync(Class eventClass, Object... } // async: generate an event class and post it - this.eventBus.getPlugin().getBootstrap().getScheduler().executeAsync(() -> { - T event = generate(eventClass, params); - this.eventBus.post(event); - }); + this.eventBus.getPlugin().getBootstrap().getScheduler().executeAsync(() -> post(eventClass, params)); } - private void postSync(Class eventClass, Object... params) { + private void postSync(Class eventClass, Object... params) { // if there aren't any handlers registered for our event, don't bother trying to post it if (!this.eventBus.shouldPost(eventClass)) { return; } // generate an event class and post it - T event = generate(eventClass, params); - this.eventBus.post(event); + post(eventClass, params); } - private boolean postCancellable(Class eventClass, Object... params) { + private boolean postCancellable(Class eventClass, Object... params) { + if (!Cancellable.class.isAssignableFrom(eventClass)) { + throw new RuntimeException("Event is not cancellable: " + eventClass.getName()); + } + // extract the initial state from the first parameter boolean initialState = (boolean) params[0]; @@ -159,23 +169,14 @@ private boolean postCancellable(Class T generate(Class eventClass, Object... params) { - try { - return (T) GeneratedEventClass.generate(eventClass).newInstance(this.eventBus.getApiProvider(), params); - } catch (Throwable e) { - throw new RuntimeException("Exception occurred whilst generating event instance", e); - } - } public void dispatchContextUpdate(Object subject) { postSync(ContextUpdateEvent.class, subject); @@ -225,12 +226,16 @@ public void dispatchLogReceive(UUID id, Action entry) { postAsync(LogReceiveEvent.class, id, entry); } - public void dispatchNodeChanges(PermissionHolder target, DataType dataType, MutateResult changes) { - if (!this.eventBus.shouldPost(NodeAddEvent.class) && !this.eventBus.shouldPost(NodeRemoveEvent.class)) { + public void dispatchCustomMessageReceive(String channelId, String payload) { + postAsync(CustomMessageReceiveEvent.class, channelId, payload); + } + + public void dispatchNodeChanges(PermissionHolder target, DataType dataType, Difference changes) { + if (changes.isEmpty()) { return; } - if (changes.isEmpty()) { + if (!this.eventBus.shouldPost(NodeAddEvent.class) && !this.eventBus.shouldPost(NodeRemoveEvent.class)) { return; } @@ -238,15 +243,15 @@ public void dispatchNodeChanges(PermissionHolder target, DataType dataType, Muta ImmutableSet state = target.getData(dataType).asImmutableSet(); // call an event for each recorded change - for (MutateResult.Change change : changes.getChanges()) { - Class type = change.getType() == MutateResult.ChangeType.ADD ? + for (Difference.Change change : changes.getChanges()) { + Class type = change.type() == Difference.ChangeType.ADD ? NodeAddEvent.class : NodeRemoveEvent.class; - postAsync(type, proxy, dataType, state, change.getNode()); + postAsync(type, proxy, dataType, state, change.value()); } } - public void dispatchNodeClear(PermissionHolder target, DataType dataType, MutateResult changes) { + public void dispatchNodeClear(PermissionHolder target, DataType dataType, Difference changes) { if (!this.eventBus.shouldPost(NodeClearEvent.class)) { return; } @@ -272,12 +277,16 @@ public void dispatchConfigReload() { postAsync(ConfigReloadEvent.class); } + public void dispatchNetworkPostSync(UUID id, SyncType type, boolean didOccur, UUID specificUserUniqueId) { + postAsync(PostNetworkSyncEvent.class, id, type, didOccur, specificUserUniqueId); + } + public void dispatchPostSync() { postAsync(PostSyncEvent.class); } - public boolean dispatchNetworkPreSync(boolean initialState, UUID id) { - return postCancellable(PreNetworkSyncEvent.class, initialState, id); + public boolean dispatchNetworkPreSync(boolean initialState, UUID id, SyncType type, UUID specificUserUniqueId) { + return postCancellable(PreNetworkSyncEvent.class, initialState, id, type, specificUserUniqueId); } public boolean dispatchPreSync(boolean initialState) { @@ -390,8 +399,9 @@ private static ApiPermissionHolder proxy(PermissionHolder holder) { } } - public static List> getKnownEventTypes() { - return ImmutableList.of( + @SuppressWarnings("unchecked") + public static Class[] getKnownEventTypes() { + return new Class[]{ ContextUpdateEvent.class, ExtensionLoadEvent.class, GroupCacheLoadEvent.class, @@ -405,6 +415,7 @@ public static List> getKnownEventTypes() { LogNotifyEvent.class, LogPublishEvent.class, LogReceiveEvent.class, + CustomMessageReceiveEvent.class, NodeAddEvent.class, NodeClearEvent.class, NodeRemoveEvent.class, @@ -415,6 +426,7 @@ public static List> getKnownEventTypes() { UsernameLookupEvent.class, UsernameValidityCheckEvent.class, ConfigReloadEvent.class, + PostNetworkSyncEvent.class, PostSyncEvent.class, PreNetworkSyncEvent.class, PreSyncEvent.class, @@ -432,7 +444,7 @@ public static List> getKnownEventTypes() { UserUnloadEvent.class, UserDemoteEvent.class, UserPromoteEvent.class - ); + }; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/event/LuckPermsEventSubscription.java b/common/src/main/java/me/lucko/luckperms/common/event/LuckPermsEventSubscription.java index 2df785fc5..bc0c06665 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/LuckPermsEventSubscription.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/LuckPermsEventSubscription.java @@ -28,7 +28,6 @@ import net.kyori.event.EventSubscriber; import net.luckperms.api.event.EventSubscription; import net.luckperms.api.event.LuckPermsEvent; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/event/gen/AbstractEvent.java b/common/src/main/java/me/lucko/luckperms/common/event/gen/AbstractEvent.java index 890fd6a20..5dd3b120e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/gen/AbstractEvent.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/gen/AbstractEvent.java @@ -27,7 +27,6 @@ import net.luckperms.api.LuckPerms; import net.luckperms.api.event.LuckPermsEvent; - import org.checkerframework.checker.nullness.qual.NonNull; import java.lang.invoke.MethodHandles; diff --git a/common/src/main/java/me/lucko/luckperms/common/event/gen/GeneratedEventClass.java b/common/src/main/java/me/lucko/luckperms/common/event/gen/GeneratedEventClass.java index 54fe9fa00..248f929e5 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/gen/GeneratedEventClass.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/gen/GeneratedEventClass.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.cache.LoadingMap; import me.lucko.luckperms.common.event.EventDispatcher; - import net.bytebuddy.ByteBuddy; import net.bytebuddy.ClassFileVersion; import net.bytebuddy.description.NamedElement; diff --git a/common/src/main/java/me/lucko/luckperms/common/event/listeners/AbstractAutoOpListener.java b/common/src/main/java/me/lucko/luckperms/common/event/listeners/AbstractAutoOpListener.java new file mode 100644 index 000000000..a745128f8 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/event/listeners/AbstractAutoOpListener.java @@ -0,0 +1,103 @@ +/* + * 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.event.listeners; + +import me.lucko.luckperms.common.api.implementation.ApiUser; +import me.lucko.luckperms.common.context.manager.ContextManager; +import me.lucko.luckperms.common.event.LuckPermsEventListener; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import net.luckperms.api.event.EventBus; +import net.luckperms.api.event.context.ContextUpdateEvent; +import net.luckperms.api.event.user.UserDataRecalculateEvent; +import net.luckperms.api.query.QueryOptions; + +import java.util.Map; +import java.util.UUID; + +/** + * Implements the LuckPerms auto op feature. + */ +public abstract class AbstractAutoOpListener

    implements LuckPermsEventListener { + private static final String NODE = "luckperms.autoop"; + + protected final P plugin; + private final ContextManager contextManager; + private final Class playerClass; + + public AbstractAutoOpListener(P plugin, ContextManager contextManager, Class playerClass) { + this.plugin = plugin; + this.contextManager = contextManager; + this.playerClass = playerClass; + } + + protected abstract boolean isServerAvailable(); + protected abstract UUID getUniqueId(T player); + protected abstract void setOp(T player, boolean value, boolean callerIsSync); + + @Override + public final void bind(EventBus bus) { + bus.subscribe(ContextUpdateEvent.class, this::onContextUpdate); + bus.subscribe(UserDataRecalculateEvent.class, this::onUserDataRecalculate); + } + + private void onContextUpdate(ContextUpdateEvent event) { + event.getSubject(this.playerClass).ifPresent(player -> refreshAutoOp(player, true)); + } + + private void onUserDataRecalculate(UserDataRecalculateEvent event) { + User user = ApiUser.cast(event.getUser()); + T player = getPlayerByUuid(user.getUniqueId()); + if (player != null) { + refreshAutoOp(player, false); + } + } + + @SuppressWarnings("unchecked") + private T getPlayerByUuid(UUID uuid) { + return (T) this.plugin.getBootstrap().getPlayer(uuid).orElse(null); + } + + private void refreshAutoOp(T player, boolean callerIsSync) { + if (!callerIsSync && !isServerAvailable()) { + return; + } + + User user = this.plugin.getUserManager().getIfLoaded(getUniqueId(player)); + + boolean value; + if (user != null) { + QueryOptions queryOptions = this.contextManager.getQueryOptions(player); + Map permData = user.getCachedData().getPermissionData(queryOptions).getPermissionMap(); + value = permData.getOrDefault(NODE, false); + } else { + value = false; + } + + setOp(player, value, callerIsSync); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/event/listeners/AbstractCommandListUpdater.java b/common/src/main/java/me/lucko/luckperms/common/event/listeners/AbstractCommandListUpdater.java new file mode 100644 index 000000000..abef2ed04 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/event/listeners/AbstractCommandListUpdater.java @@ -0,0 +1,114 @@ +/* + * 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.event.listeners; + +import com.github.benmanes.caffeine.cache.LoadingCache; +import me.lucko.luckperms.common.api.implementation.ApiGroup; +import me.lucko.luckperms.common.cache.BufferedRequest; +import me.lucko.luckperms.common.event.LuckPermsEventListener; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.util.CaffeineFactory; +import net.luckperms.api.event.EventBus; +import net.luckperms.api.event.context.ContextUpdateEvent; +import net.luckperms.api.event.group.GroupDataRecalculateEvent; +import net.luckperms.api.event.user.UserDataRecalculateEvent; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Re-sends the command list/tree when a players permissions change. + */ +public abstract class AbstractCommandListUpdater

    implements LuckPermsEventListener { + protected final P plugin; + private final Class playerClass; + private final LoadingCache sendingBuffers = CaffeineFactory.newBuilder() + .expireAfterAccess(10, TimeUnit.SECONDS) + .build(SendBuffer::new); + + public AbstractCommandListUpdater(P plugin, Class playerClass) { + this.plugin = plugin; + this.playerClass = playerClass; + } + + protected abstract boolean isServerAvailable(); + protected abstract UUID getUniqueId(T player); + + // Called when the buffer times out. + protected abstract void sendCommandListUpdate(UUID uniqueId); + + @Override + public final void bind(EventBus bus) { + bus.subscribe(UserDataRecalculateEvent.class, this::onUserDataRecalculate); + bus.subscribe(GroupDataRecalculateEvent.class, this::onGroupDataRecalculate); + bus.subscribe(ContextUpdateEvent.class, this::onContextUpdate); + } + + private void onUserDataRecalculate(UserDataRecalculateEvent e) { + requestUpdate(e.getUser().getUniqueId()); + } + + private void onGroupDataRecalculate(GroupDataRecalculateEvent e) { + for (User user : this.plugin.getUserManager().getAll().values()) { + if (user.resolveInheritanceTree(user.getQueryOptions()).contains(ApiGroup.cast(e.getGroup()))) { + requestUpdate(user.getUniqueId()); + } + } + } + + private void onContextUpdate(ContextUpdateEvent e) { + e.getSubject(this.playerClass).ifPresent(p -> requestUpdate(getUniqueId(p))); + } + + private void requestUpdate(UUID uniqueId) { + if (!isServerAvailable()) { + return; + } + + if (!this.plugin.getBootstrap().isPlayerOnline(uniqueId)) { + return; + } + + this.sendingBuffers.get(uniqueId).request(); + } + + private final class SendBuffer extends BufferedRequest { + private final UUID uniqueId; + + SendBuffer(UUID uniqueId) { + super(500, TimeUnit.MILLISECONDS, AbstractCommandListUpdater.this.plugin.getBootstrap().getScheduler()); + this.uniqueId = uniqueId; + } + + @Override + protected Void perform() { + sendCommandListUpdate(this.uniqueId); + return null; + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/event/model/EntitySourceImpl.java b/common/src/main/java/me/lucko/luckperms/common/event/model/EntitySourceImpl.java index ec4c85741..3919cd827 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/model/EntitySourceImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/model/EntitySourceImpl.java @@ -27,7 +27,6 @@ import net.luckperms.api.event.source.EntitySource; import net.luckperms.api.platform.PlatformEntity; - import org.checkerframework.checker.nullness.qual.NonNull; public class EntitySourceImpl implements EntitySource { diff --git a/common/src/main/java/me/lucko/luckperms/common/event/model/SenderPlatformEntity.java b/common/src/main/java/me/lucko/luckperms/common/event/model/SenderPlatformEntity.java index bd2f48425..aac1b5261 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/model/SenderPlatformEntity.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/model/SenderPlatformEntity.java @@ -26,9 +26,7 @@ package me.lucko.luckperms.common.event.model; import me.lucko.luckperms.common.sender.Sender; - import net.luckperms.api.platform.PlatformEntity; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/event/model/UnknownSource.java b/common/src/main/java/me/lucko/luckperms/common/event/model/UnknownSource.java index 23ff3da50..571367c8b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/event/model/UnknownSource.java +++ b/common/src/main/java/me/lucko/luckperms/common/event/model/UnknownSource.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.event.model; import net.luckperms.api.event.source.Source; - import org.checkerframework.checker.nullness.qual.NonNull; public final class UnknownSource implements Source { diff --git a/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java b/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java index 05598d03a..bf2e0a1cd 100644 --- a/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/extension/SimpleExtensionManager.java @@ -26,14 +26,11 @@ package me.lucko.luckperms.common.extension; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.util.gson.GsonProvider; - import net.luckperms.api.LuckPerms; import net.luckperms.api.extension.Extension; import net.luckperms.api.extension.ExtensionManager; - import org.checkerframework.checker.nullness.qual.NonNull; import java.io.BufferedReader; @@ -42,9 +39,6 @@ import java.io.InputStreamReader; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.URL; -import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -98,7 +92,7 @@ public void loadExtensions(Path directory) { if (path.getFileName().toString().endsWith(".jar")) { try { loadExtension(path); - } catch (IOException e) { + } catch (Exception e) { this.plugin.getLogger().warn("Exception loading extension from " + path, e); } } @@ -119,7 +113,6 @@ public void loadExtensions(Path directory) { } String className; - boolean useParentClassLoader = false; try (JarFile jar = new JarFile(path.toFile())) { JarEntry extensionJarEntry = jar.getJarEntry("extension.json"); if (extensionJarEntry == null) { @@ -132,8 +125,14 @@ public void loadExtensions(Path directory) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { JsonObject parsed = GsonProvider.parser().parse(reader).getAsJsonObject(); className = parsed.get("class").getAsString(); + + // check for deprecated flags if (parsed.has("useParentClassLoader")) { - useParentClassLoader = parsed.get("useParentClassLoader").getAsBoolean(); + boolean useParentClassLoader = parsed.get("useParentClassLoader").getAsBoolean(); + if (useParentClassLoader) { + this.plugin.getLogger().warn("Extension '" + className + "' specifies the 'useParentClassLoader' extension flag, which has been deprecated/removed. " + + "The extension will be loaded using the classloader of the LuckPerms plugin, and not the classloader of the platform, which may break functionality."); + } } } } @@ -143,15 +142,7 @@ public void loadExtensions(Path directory) { throw new IllegalArgumentException("class is null"); } - if (useParentClassLoader && isJarInJar()) { - try { - addJarToParentClasspath(path); - } catch (Exception e) { - throw new RuntimeException("Exception whilst classloading extension", e); - } - } else { - this.plugin.getBootstrap().getClassPathAppender().addJarToClasspath(path); - } + this.plugin.getBootstrap().getClassPathAppender().addJarToClasspath(path); Class extensionClass; try { @@ -195,23 +186,6 @@ public void loadExtensions(Path directory) { return this.extensions.stream().map(e -> e.instance).collect(Collectors.toSet()); } - private static boolean isJarInJar() { - String thisClassLoaderName = SimpleExtensionManager.class.getClassLoader().getClass().getName(); - return thisClassLoaderName.equals("me.lucko.luckperms.common.loader.JarInJarClassLoader"); - } - - private static void addJarToParentClasspath(Path path) throws Exception { - ClassLoader parentClassLoader = SimpleExtensionManager.class.getClassLoader().getParent(); - if (!(parentClassLoader instanceof URLClassLoader)) { - throw new RuntimeException("useParentClassLoader is true but parent is not a URLClassLoader"); - } - - Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - addUrlMethod.setAccessible(true); - - addUrlMethod.invoke(parentClassLoader, path.toUri().toURL()); - } - private static final class LoadedExtension { private final Extension instance; private final Path path; diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Comparison.java b/common/src/main/java/me/lucko/luckperms/common/filter/Comparison.java similarity index 56% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Comparison.java rename to common/src/main/java/me/lucko/luckperms/common/filter/Comparison.java index c05fed3d1..91456dfe4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Comparison.java +++ b/common/src/main/java/me/lucko/luckperms/common/filter/Comparison.java @@ -23,48 +23,60 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.bulkupdate.comparison; +package me.lucko.luckperms.common.filter; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; +import java.util.regex.Pattern; /** * A method of comparing two strings */ -public interface Comparison { +public enum Comparison { + + EQUAL("=="), + NOT_EQUAL("!="), + SIMILAR("~~"), + NOT_SIMILAR("!~"); + + public static final String WILDCARD = "%"; + public static final String WILDCARD_ONE = "_"; + + private final String symbol; + + Comparison(String symbol) { + this.symbol = symbol; + } /** * Gets the symbol which represents this comparison * * @return the comparison symbol */ - String getSymbol(); + public String getSymbol() { + return this.symbol; + } - /** - * Creates a {@link CompiledExpression} for the given expression - * - * @param expression the expression - * @return the compiled expression - */ - CompiledExpression compile(String expression); + @Override + public String toString() { + return this.symbol; + } - /** - * Returns the comparison operator in SQL form - */ - void appendSql(PreparedStatementBuilder builder); + public static Comparison parse(String s) { + for (Comparison t : values()) { + if (t.getSymbol().equals(s)) { + return t; + } + } + return null; + } - /** - * An instance of {@link Comparison} which is bound to an expression. - */ - interface CompiledExpression { + public static Pattern compilePatternForLikeSyntax(String expression) { + expression = expression.replace(".", "\\."); + + // convert from SQL LIKE syntax to regex + expression = expression.replace(WILDCARD_ONE, "."); + expression = expression.replace(WILDCARD, ".*"); - /** - * Tests the expression against a given string, according to the - * rules of the parent {@link Comparison}. - * - * @param string the string - * @return if there was a match - */ - boolean test(String string); + return Pattern.compile(expression, Pattern.CASE_INSENSITIVE); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Constraint.java b/common/src/main/java/me/lucko/luckperms/common/filter/Constraint.java similarity index 60% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Constraint.java rename to common/src/main/java/me/lucko/luckperms/common/filter/Constraint.java index d08b1e252..8386509c2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/comparison/Constraint.java +++ b/common/src/main/java/me/lucko/luckperms/common/filter/Constraint.java @@ -23,24 +23,28 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.bulkupdate.comparison; +package me.lucko.luckperms.common.filter; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; +import java.util.function.Predicate; -public class Constraint { - - public static Constraint of(Comparison comparison, String expression) { - return new Constraint(comparison, expression); - } +public class Constraint { private final Comparison comparison; - private final String expressionValue; - private final Comparison.CompiledExpression compiledExpression; + private final T value; + private final Predicate predicate; - private Constraint(Comparison comparison, String expression) { + Constraint(Comparison comparison, T value, Predicate predicate) { this.comparison = comparison; - this.expressionValue = expression; - this.compiledExpression = this.comparison.compile(this.expressionValue); + this.value = value; + this.predicate = predicate; + } + + public Comparison comparison() { + return this.comparison; + } + + public T value() { + return this.value; } /** @@ -49,20 +53,12 @@ private Constraint(Comparison comparison, String expression) { * @param value the value * @return true if satisfied */ - public boolean eval(String value) { - return this.compiledExpression.test(value); - } - - public void appendSql(PreparedStatementBuilder builder, String field) { - // e.g. field LIKE ? - builder.append(field).append(' '); - this.comparison.appendSql(builder); - builder.append(' '); - builder.variable(this.expressionValue); + public boolean evaluate(T value) { + return this.predicate.test(value); } @Override public String toString() { - return this.comparison + " " + this.expressionValue; + return this.comparison + " " + this.value; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/ConstraintFactory.java b/common/src/main/java/me/lucko/luckperms/common/filter/ConstraintFactory.java new file mode 100644 index 000000000..a7897384a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/ConstraintFactory.java @@ -0,0 +1,102 @@ +/* + * 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.filter; + +import java.util.UUID; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public interface ConstraintFactory { + + Predicate equal(T value); + Predicate notEqual(T value); + Predicate similar(T value); + Predicate notSimilar(T value); + + default Constraint build(Comparison comparison, T value) { + switch (comparison) { + case EQUAL: + return new Constraint<>(comparison, value, equal(value)); + case NOT_EQUAL: + return new Constraint<>(comparison, value, notEqual(value)); + case SIMILAR: + return new Constraint<>(comparison, value, similar(value)); + case NOT_SIMILAR: + return new Constraint<>(comparison, value, notSimilar(value)); + default: + throw new AssertionError(comparison); + } + } + + ConstraintFactory STRINGS = new ConstraintFactory() { + @Override + public Predicate equal(String value) { + return value::equalsIgnoreCase; + } + + @Override + public Predicate notEqual(String value) { + return string -> !value.equalsIgnoreCase(string); + } + + @Override + public Predicate similar(String value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value); + return string -> pattern.matcher(string).matches(); + } + + @Override + public Predicate notSimilar(String value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value); + return string -> !pattern.matcher(string).matches(); + } + }; + + ConstraintFactory UUIDS = new ConstraintFactory() { + @Override + public Predicate equal(UUID value) { + return value::equals; + } + + @Override + public Predicate notEqual(UUID value) { + return string -> !value.equals(string); + } + + @Override + public Predicate similar(UUID value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value.toString()); + return uuid -> pattern.matcher(uuid.toString()).matches(); + } + + @Override + public Predicate notSimilar(UUID value) { + Pattern pattern = Comparison.compilePatternForLikeSyntax(value.toString()); + return uuid -> !pattern.matcher(uuid.toString()).matches(); + } + }; + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/Filter.java b/common/src/main/java/me/lucko/luckperms/common/filter/Filter.java new file mode 100644 index 000000000..8270ff0d0 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/Filter.java @@ -0,0 +1,59 @@ +/* + * 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.filter; + +public class Filter { + private final FilterField field; + private final Constraint constraint; + + public Filter(FilterField field, Constraint constraint) { + this.field = field; + this.constraint = constraint; + } + + public final FilterField field() { + return this.field; + } + + public final Constraint constraint() { + return this.constraint; + } + + /** + * Returns if the given value satisfies this filter + * + * @param value the value + * @return true if satisfied + */ + public boolean evaluate(T value) { + return this.constraint.evaluate(this.field.getValue(value)); + } + + @Override + public String toString() { + return this.field + " " + this.constraint; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/FilterField.java b/common/src/main/java/me/lucko/luckperms/common/filter/FilterField.java new file mode 100644 index 000000000..a0dd64b3f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/FilterField.java @@ -0,0 +1,74 @@ +/* + * 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.filter; + +import java.util.function.Function; + +/** + * Represents a field that can be filtered on. + * + * @param the type the field is on + */ +public interface FilterField { + + static FilterField named(String name, Function func) { + return new FilterField() { + @Override + public FT getValue(T object) { + return func.apply(object); + } + + @Override + public String toString() { + return name; + } + }; + } + + /** + * Gets the value of this field on the given object. + * + * @param object the object + * @return the field value as a string + */ + FT getValue(T object); + + default Filter isEqualTo(FT value, ConstraintFactory factory) { + return new Filter<>(this, factory.build(Comparison.EQUAL, value)); + } + + default Filter isNotEqualTo(FT value, ConstraintFactory factory) { + return new Filter<>(this, factory.build(Comparison.NOT_EQUAL, value)); + } + + default Filter isSimilarTo(FT value, ConstraintFactory factory) { + return new Filter<>(this, factory.build(Comparison.SIMILAR, value)); + } + + default Filter isNotSimilarTo(FT value, ConstraintFactory factory) { + return new Filter<>(this, factory.build(Comparison.NOT_SIMILAR, value)); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/FilterList.java b/common/src/main/java/me/lucko/luckperms/common/filter/FilterList.java new file mode 100644 index 000000000..c70d4d32c --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/FilterList.java @@ -0,0 +1,101 @@ +/* + * 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.filter; + +import com.google.common.collect.ForwardingList; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +public class FilterList extends ForwardingList> { + + public static FilterList empty() { + return new FilterList<>(LogicalOperator.AND, ImmutableList.of()); + } + + @SafeVarargs + public static FilterList and(Filter... filters) { + return new FilterList<>(LogicalOperator.AND, ImmutableList.copyOf(filters)); + } + + @SafeVarargs + public static FilterList or(Filter... filters) { + return new FilterList<>(LogicalOperator.OR, ImmutableList.copyOf(filters)); + } + + private final LogicalOperator operator; + private final List> filters; + + public FilterList(LogicalOperator operator, List> filters) { + this.operator = operator; + this.filters = filters; + } + + public LogicalOperator operator() { + return this.operator; + } + + @Override + protected List> delegate() { + return this.filters; + } + + /** + * Check to see if a value satisfies all (AND) or any (OR) filters in the list + * + * @param value the value to check + * @return true if satisfied + */ + public boolean evaluate(T value) { + return this.operator.match(this.filters, value); + } + + @Override + public String toString() { + String operator = this.operator.name().toLowerCase(Locale.ROOT); + return this.filters.stream().map(Filter::toString).collect(Collectors.joining(" " + operator + " ")); + } + + public enum LogicalOperator { + AND { + @Override + public boolean match(List> filters, T value) { + return filters.stream().allMatch(filter -> filter.evaluate(value)); // true if empty + } + }, + OR { + @Override + public boolean match(List> filters, T value) { + return filters.stream().anyMatch(filter -> filter.evaluate(value)); // false if empty + } + }; + + public abstract boolean match(List> filters, T value); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/PageParameters.java b/common/src/main/java/me/lucko/luckperms/common/filter/PageParameters.java new file mode 100644 index 000000000..ce61c3df2 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/PageParameters.java @@ -0,0 +1,79 @@ +/* + * 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.filter; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +public class PageParameters { + + private final int pageSize; + private final int pageNumber; + + public PageParameters(int pageSize, int pageNumber) { + if (pageSize < 1) { + throw new IllegalArgumentException("pageSize cannot be less than 1: " + pageSize); + } + if (pageNumber < 1) { + throw new IllegalArgumentException("pageNumber cannot be less than 1: " + pageNumber); + } + + this.pageSize = pageSize; + this.pageNumber = pageNumber; + } + + public int pageSize() { + return this.pageSize; + } + + public int pageNumber() { + return this.pageNumber; + } + + public List paginate(List input) { + int fromIndex = this.pageSize * (this.pageNumber - 1); + if (fromIndex >= input.size()) { + return Collections.emptyList(); + } + + int toIndex = Math.min(fromIndex + this.pageSize, input.size()); + return input.subList(fromIndex, toIndex); + } + + public Stream paginate(Stream input) { + return input.skip((long) this.pageSize * (this.pageNumber - 1)).limit(this.pageSize); + } + + public int getMaxPage(int totalEntries) { + if (totalEntries == 0) { + return 0; + } + + return (totalEntries + this.pageSize - 1) / this.pageSize; + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/mongo/ConstraintMongoBuilder.java b/common/src/main/java/me/lucko/luckperms/common/filter/mongo/ConstraintMongoBuilder.java new file mode 100644 index 000000000..5d37061bd --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/mongo/ConstraintMongoBuilder.java @@ -0,0 +1,87 @@ +/* + * 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.filter.mongo; + +import com.mongodb.client.FindIterable; +import com.mongodb.client.model.Filters; +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.Constraint; +import me.lucko.luckperms.common.filter.PageParameters; +import org.bson.conversions.Bson; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.regex.Pattern; + +public class ConstraintMongoBuilder { + public static final ConstraintMongoBuilder INSTANCE = new ConstraintMongoBuilder(); + + protected ConstraintMongoBuilder() { + + } + + public Object mapConstraintValue(Object value) { + return value; + } + + public Bson make(Constraint constraint, String fieldName) { + Comparison comparison = constraint.comparison(); + Object value = mapConstraintValue(constraint.value()); + + switch (comparison) { + case EQUAL: + return Filters.eq(fieldName, value); + case NOT_EQUAL: + return Filters.ne(fieldName, value); + case SIMILAR: { + if (!(value instanceof String)) { + throw new IllegalArgumentException("Unable to create SIMILAR comparison for non-string type: " + value.getClass().getName()); + } + Pattern pattern = Comparison.compilePatternForLikeSyntax((String) value); + return Filters.regex(fieldName, pattern); + } + case NOT_SIMILAR: { + if (!(value instanceof String)) { + throw new IllegalArgumentException("Unable to create NOT_SIMILAR comparison for non-string type: " + value.getClass().getName()); + } + Pattern pattern = Comparison.compilePatternForLikeSyntax((String) value); + return Filters.not(Filters.regex(fieldName, pattern)); + } + default: + throw new AssertionError(comparison); + } + } + + public static FindIterable page(@Nullable PageParameters params, FindIterable iterable) { + if (params == null) { + return iterable; + } + + int pageSize = params.pageSize(); + int pageNumber = params.pageNumber(); + return iterable.limit(pageSize).skip((pageNumber - 1) * pageSize); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/mongo/FilterMongoBuilder.java b/common/src/main/java/me/lucko/luckperms/common/filter/mongo/FilterMongoBuilder.java new file mode 100644 index 000000000..93cd57267 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/mongo/FilterMongoBuilder.java @@ -0,0 +1,65 @@ +/* + * 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.filter.mongo; + +import com.mongodb.client.model.Filters; +import me.lucko.luckperms.common.filter.Filter; +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.FilterList; +import org.bson.conversions.Bson; + +import java.util.List; +import java.util.stream.Collectors; + +public abstract class FilterMongoBuilder extends ConstraintMongoBuilder { + + public abstract String mapFieldName(FilterField field); + + public Bson make(Filter filter) { + return make(filter.constraint(), mapFieldName(filter.field())); + } + + public Bson make(FilterList.LogicalOperator combineOperator, List> filters) { + if (filters.isEmpty()) { + return Filters.empty(); + } + + List bsonFilters = filters.stream().map(this::make).collect(Collectors.toList()); + switch (combineOperator) { + case AND: + return Filters.and(bsonFilters); + case OR: + return Filters.or(bsonFilters); + default: + throw new AssertionError(combineOperator); + } + } + + public Bson make(FilterList filters) { + return make(filters.operator(), filters); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/sql/ConstraintSqlBuilder.java b/common/src/main/java/me/lucko/luckperms/common/filter/sql/ConstraintSqlBuilder.java new file mode 100644 index 000000000..04e5d7fc5 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/sql/ConstraintSqlBuilder.java @@ -0,0 +1,84 @@ +/* + * 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.filter.sql; + +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.Constraint; +import me.lucko.luckperms.common.filter.PageParameters; +import me.lucko.luckperms.common.storage.implementation.sql.builder.AbstractSqlBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ConstraintSqlBuilder extends AbstractSqlBuilder { + + public void visitConstraintValue(Object value) { + if (value instanceof String) { + this.builder.variable(((String) value)); + } else { + throw new IllegalArgumentException("Don't know how to write value with type: " + value.getClass().getName()); + } + } + + public void visit(Constraint constraint) { + // '= value' + // '!= value' + // 'LIKE value' + // 'NOT LIKE value' + + visit(constraint.comparison()); + this.builder.append(' '); + visitConstraintValue(constraint.value()); + } + + public void visit(Comparison comparison) { + switch (comparison) { + case EQUAL: + this.builder.append("="); + break; + case NOT_EQUAL: + this.builder.append("!="); + break; + case SIMILAR: + this.builder.append("LIKE"); + break; + case NOT_SIMILAR: + this.builder.append("NOT LIKE"); + break; + default: + throw new AssertionError(comparison); + } + } + + public void visit(@Nullable PageParameters params) { + if (params == null) { + return; + } + + int pageSize = params.pageSize(); + int pageNumber = params.pageNumber(); + this.builder.append(" LIMIT " + pageSize + " OFFSET " + (pageNumber - 1) * pageSize); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/filter/sql/FilterSqlBuilder.java b/common/src/main/java/me/lucko/luckperms/common/filter/sql/FilterSqlBuilder.java new file mode 100644 index 000000000..e5c643698 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/filter/sql/FilterSqlBuilder.java @@ -0,0 +1,81 @@ +/* + * 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.filter.sql; + +import me.lucko.luckperms.common.filter.Filter; +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.FilterList; + +import java.util.List; + +public abstract class FilterSqlBuilder extends ConstraintSqlBuilder { + + public abstract void visitFieldName(FilterField field); + + public void visit(Filter filter) { + // 'field = value' + // 'field != value' + // 'field LIKE value' + // 'field NOT LIKE value' + + visitFieldName(filter.field()); + this.builder.append(' '); + visit(filter.constraint()); + } + + public void visit(FilterList.LogicalOperator combineOperator, List> filters) { + if (filters.isEmpty()) { + return; + } + + String combineString; + switch (combineOperator) { + case AND: + combineString = "AND "; + break; + case OR: + combineString = "OR "; + break; + default: + throw new AssertionError(combineOperator); + } + + this.builder.append(" WHERE"); + for (int i = 0; i < filters.size(); i++) { + Filter filter = filters.get(i); + this.builder.append(" "); + if (i != 0) { + this.builder.append(combineString); + } + visit(filter); + } + } + + public void visit(FilterList filters) { + visit(filters.operator(), filters); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/http/BytebinClient.java b/common/src/main/java/me/lucko/luckperms/common/http/BytebinClient.java index 94326efd4..38b7978dd 100644 --- a/common/src/main/java/me/lucko/luckperms/common/http/BytebinClient.java +++ b/common/src/main/java/me/lucko/luckperms/common/http/BytebinClient.java @@ -26,9 +26,7 @@ package me.lucko.luckperms.common.http; import com.google.gson.JsonElement; - import me.lucko.luckperms.common.util.gson.GsonProvider; - import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -83,15 +81,21 @@ public Response makeHttpRequest(Request request) throws IOException, Unsuccessfu * * @param buf the compressed content * @param contentType the type of the content + * @param userAgentExtra extra string to append to the user agent * @return the key of the resultant content * @throws IOException if an error occurs */ - public Content postContent(byte[] buf, MediaType contentType) throws IOException, UnsuccessfulRequestException { + public Content postContent(byte[] buf, MediaType contentType, String userAgentExtra) throws IOException, UnsuccessfulRequestException { RequestBody body = RequestBody.create(contentType, buf); + String userAgent = this.userAgent; + if (userAgentExtra != null) { + userAgent += "/" + userAgentExtra; + } + Request.Builder requestBuilder = new Request.Builder() .url(this.url + "post") - .header("User-Agent", this.userAgent) + .header("User-Agent", userAgent) .header("Content-Encoding", "gzip"); Request request = requestBuilder.post(body).build(); @@ -104,6 +108,10 @@ public Content postContent(byte[] buf, MediaType contentType) throws IOException } } + public Content postContent(byte[] buf, MediaType contentType) throws IOException, UnsuccessfulRequestException { + return postContent(buf, contentType, null); + } + /** * GETs json content from bytebin * 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 new file mode 100644 index 000000000..470bb5ddf --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/http/BytesocksClient.java @@ -0,0 +1,108 @@ +/* + * 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.http; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + +import java.io.IOException; +import java.util.Objects; + +public class BytesocksClient extends AbstractHttpClient { + + /* The bytesocks urls */ + private final String httpUrl; + private final String wsUrl; + + /** The client user agent */ + private final String userAgent; + + /** + * Creates a new bytesocks instance + * + * @param url the bytesocks url + * @param userAgent the client user agent string + */ + public BytesocksClient(OkHttpClient okHttpClient, String url, String userAgent) { + super(okHttpClient); + + this.httpUrl = url; + if (this.httpUrl.startsWith("http://")) { + this.wsUrl = "ws://" + this.httpUrl.substring(7); + } else if (this.httpUrl.startsWith("https://")) { + this.wsUrl = "wss://" + this.httpUrl.substring(8); + } else { + throw new IllegalArgumentException("Invalid URL: " + url); + } + + this.userAgent = userAgent; + } + + public Socket createSocket(WebSocketListener listener) throws IOException, UnsuccessfulRequestException { + Request createRequest = new Request.Builder() + .url(this.httpUrl + "create") + .header("User-Agent", this.userAgent) + .build(); + + String id; + try (Response response = makeHttpRequest(createRequest)) { + if (response.code() != 201) { + throw new UnsuccessfulRequestException(response); + } + + id = Objects.requireNonNull(response.header("Location")); + } + + Request socketRequest = new Request.Builder() + .url(this.wsUrl + id) + .header("User-Agent", this.userAgent) + .build(); + + return new Socket(id, this.okHttp.newWebSocket(socketRequest, listener)); + } + + public static final class Socket { + private final String channelId; + private final WebSocket socket; + + public Socket(String channelId, WebSocket socket) { + this.channelId = channelId; + this.socket = socket; + } + + public String channelId() { + return this.channelId; + } + + public WebSocket socket() { + return this.socket; + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraph.java b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraph.java index 4474b0ef0..c463b1381 100644 --- a/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraph.java +++ b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraph.java @@ -31,7 +31,6 @@ import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.node.types.InheritanceNode; import net.luckperms.api.query.QueryOptions; diff --git a/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraphFactory.java b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraphFactory.java index 61fd32b33..ce508626f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraphFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/inheritance/InheritanceGraphFactory.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.query.QueryOptionsImpl; - import net.luckperms.api.query.QueryOptions; /** 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 f6c51ae5c..8634e9845 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 @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.locale; import com.google.common.collect.Maps; - import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; @@ -37,13 +36,15 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.storage.StorageMetadata; 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; @@ -51,12 +52,19 @@ import net.luckperms.api.node.Node; import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; import net.luckperms.api.node.types.ChatMetaNode; +import net.luckperms.api.node.types.DisplayNameNode; import net.luckperms.api.node.types.InheritanceNode; import net.luckperms.api.node.types.MetaNode; +import net.luckperms.api.node.types.PrefixNode; +import net.luckperms.api.node.types.SuffixNode; +import net.luckperms.api.node.types.WeightNode; import net.luckperms.api.util.Tristate; +import java.text.DecimalFormat; import java.time.Duration; import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -72,9 +80,11 @@ import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.translatable; import static net.kyori.adventure.text.format.NamedTextColor.AQUA; +import static net.kyori.adventure.text.format.NamedTextColor.BLUE; import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; import static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY; import static net.kyori.adventure.text.format.NamedTextColor.DARK_GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_PURPLE; import static net.kyori.adventure.text.format.NamedTextColor.DARK_RED; import static net.kyori.adventure.text.format.NamedTextColor.GOLD; import static net.kyori.adventure.text.format.NamedTextColor.GRAY; @@ -90,10 +100,13 @@ */ public interface Message { + DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd '@' HH:mm:ss") + .withZone(ZoneId.systemDefault()); + TextComponent OPEN_BRACKET = Component.text('('); TextComponent CLOSE_BRACKET = Component.text(')'); TextComponent FULL_STOP = Component.text('.'); - + Component PREFIX_COMPONENT = text() .color(GRAY) .append(text('[')) @@ -120,19 +133,23 @@ static TextComponent prefixed(ComponentLike component) { .append(text("v" + bootstrap.getVersion(), AQUA)) .build(); - Component infoLine2 = text() + String platformName = bootstrap.getType().getFriendlyName(); + String serverBrand = bootstrap.getServerBrand(); + + TextComponent.Builder infoLine2 = text() .color(DARK_GRAY) .append(text("Running on ")) - .append(text(bootstrap.getType().getFriendlyName())) - .append(text(" - ")) - .append(text(bootstrap.getServerBrand())) - .build(); + .append(text(platformName)); + + if (!platformName.equals(serverBrand)) { + infoLine2.append(text(" - ")).append(text(serverBrand)); + } // " __ " // " | |__) " // " |___ | " - return join(newline(), + return joinNewline( text() .append(text(" ", AQUA)) .append(text(" __ ", DARK_AQUA)) @@ -172,7 +189,14 @@ static TextComponent prefixed(ComponentLike component) { .color(GRAY) ); - Args2 FIRST_TIME_SETUP = (label, username) -> join(newline(), + Args0 COMMANDS_DISABLED = () -> prefixed(translatable() + // "&3LuckPerms commands are disabled." + .key("luckperms.commandsystem.commands-disabled") + .color(DARK_AQUA) + .append(FULL_STOP) + ); + + 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:" @@ -275,7 +299,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() @@ -293,7 +317,7 @@ static TextComponent prefixed(ComponentLike component) { .append(text() .color(DARK_GRAY) .append(text('[')) - .append(text(LoggedAction.getTypeCharacter(action.getTarget().getType()), GREEN)) + .append(text(LoggedAction.getTypeString(action.getTarget().getType()), GREEN)) .append(text(']')) ) .append(space()) @@ -338,6 +362,124 @@ static TextComponent prefixed(ComponentLike component) { .append(text(result, GRAY)) ); + Args1 VERBOSE_LOG_HOVER_TYPE = type -> translatable() + // "&aType: &2{}" + .key("luckperms.logs.verbose.hover.type-key") + .color(GREEN) + .append(text(": ")) + .append(text(type, DARK_GREEN)) + .build(); + + Args1 VERBOSE_LOG_HOVER_ORIGIN = origin -> translatable() + // "&bOrigin: &2{}" + .key("luckperms.logs.verbose.hover.origin-key") + .color(AQUA) + .append(text(": ")) + .append(text(origin, DARK_GREEN)) + .build(); + + Args1 VERBOSE_LOG_HOVER_CAUSE = causeNode -> translatable() + // "&bCause: {}" + .key("luckperms.logs.verbose.hover.cause-key") + .color(AQUA) + .append(text(": ")) + .apply(builder -> { + String origin = causeNode.getMetadata(InheritanceOriginMetadata.KEY) + .map(o -> o.getOrigin().getName()).orElse("?"); + + builder.append(translatable() + .key("luckperms.command.generic.permission.check.info.directly") + .color(GRAY) + .args( + text().color(AQUA).content(origin), + text(causeNode.getKey(), AQUA), + formatBoolean(causeNode.getValue()), + formatContextSet(causeNode.getContexts()) + ) + ); + }) + .build(); + + Args1 VERBOSE_LOG_HOVER_CAUSE_META = causeNode -> translatable() + // "&bCause: {}" + .key("luckperms.logs.verbose.hover.cause-key") + .color(AQUA) + .append(text(": ")) + .apply(builder -> { + String origin = causeNode.getMetadata(InheritanceOriginMetadata.KEY) + .map(o -> o.getOrigin().getName()).orElse("?"); + + builder.append(translatable() + .key("luckperms.command.generic.permission.check.info.directly") + .color(GRAY) + .args( + text().color(AQUA).content(origin), + text(causeNode.getMetaKey(), AQUA), + formatColoredValue(causeNode.getMetaValue()), + formatContextSet(causeNode.getContexts()) + ) + ); + }) + .build(); + + Args1 VERBOSE_LOG_HOVER_CONTEXT = set -> translatable() + // "&bContext: {}" + .key("luckperms.logs.verbose.hover.context-key") + .color(AQUA) + .append(text(": ")) + .append(formatContextSet(set)) + .build(); + + Args1 VERBOSE_LOG_HOVER_THREAD = threadName -> translatable() + // "&bThread: &f{}" + .key("luckperms.logs.verbose.hover.thread-key") + .color(AQUA) + .append(text(": ")) + .append(text(threadName, WHITE)) + .build(); + + Args0 VERBOSE_LOG_HOVER_TRACE_TITLE = () -> translatable() + // "&bTrace:" + .key("luckperms.logs.verbose.hover.trace-key") + .color(AQUA) + .append(text(':')) + .build(); + + Args1 VERBOSE_LOG_HOVER_TRACE_CONTENT = content -> text(content, GRAY); + + Args1 VERBOSE_LOG_HOVER_TRACE_OVERFLOW = overflow -> text() + // "&f... and {} more" + .color(WHITE) + .content("... ") + .append(translatable() + .key("luckperms.logs.verbose.hover.overflow") + .args(text(overflow)) + ) + .build(); + + Args1 VERBOSE_LOG_HOVER_PROCESSOR = processor -> translatable() + .key("luckperms.logs.verbose.hover.processor-key") + .color(AQUA) + .append(text(": ")) + .append(text(processor, DARK_GREEN)) + .build(); + + Args0 VERBOSE_NOTIFICATION_RATE_LIMITED = () -> prefixed(text() + // "&3VB &3&l> &cNotification rate limit exceeded. Some events are not being shown. Use &7/lp verbose upload &cto see the full output." + .append(translatable("luckperms.logs.verbose-prefix", DARK_AQUA)) + .append(space()) + .append(text('>', DARK_AQUA, BOLD)) + .append(space()) + .append(translatable() + .key("luckperms.logs.verbose.rate-limit-exceeded") + .color(RED) + .args(text("/lp verbose upload", GRAY) + .clickEvent(ClickEvent.runCommand("/lp verbose upload")) + ) + .append(FULL_STOP) + ) + ); + Args1 EXPORT_LOG = msg -> prefixed(text() // "&3EXPORT &3&l> &f{}" .append(translatable("luckperms.logs.export-prefix", DARK_AQUA)) @@ -384,7 +526,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() @@ -561,7 +703,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() @@ -600,7 +742,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() @@ -626,7 +768,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() @@ -781,7 +923,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()) @@ -825,7 +967,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()) @@ -844,6 +986,42 @@ static TextComponent prefixed(ComponentLike component) { }) .build(); + 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" + prefixed(translatable() + .key("luckperms.command.editor.apply-edits.unknown-session") + .color(DARK_RED)), + prefixed(translatable() + .key("luckperms.command.editor.apply-edits.right-server-question") + .color(RED) + .args(text("/" + label + " applyedits"))), + prefixed(translatable() + .key("luckperms.command.editor.apply-edits.bypass-warning") + .color(RED) + .append(text(": ")) + .append(text("/" + label + " applyedits " + code + " --force", DARK_RED))) + ); + + 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" + prefixed(translatable() + .key("luckperms.command.editor.apply-edits.already-applied") + .color(DARK_RED)), + prefixed(translatable() + .key("luckperms.command.editor.apply-edits.how-to-avoid-conflicts") + .color(RED) + .append(FULL_STOP)), + prefixed(translatable() + .key("luckperms.command.editor.apply-edits.bypass-warning") + .color(RED) + .append(text(": ")) + .append(text("/" + label + " applyedits " + code + " --force"))) + ); + Args1 APPLY_EDITS_INVALID_CODE = code -> prefixed(text() // "&cInvalid code. &7({})" .color(RED) @@ -940,7 +1118,7 @@ static TextComponent prefixed(ComponentLike component) { Args1 APPLY_EDIT_NODE = node -> text() // "&f{} {} {} {}" .color(WHITE) - .append(text(node.getKey())) + .append(formatNodeKeyWithType(node)) .append(space()) .append(text() .color(GRAY) @@ -1008,7 +1186,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() @@ -1021,6 +1199,97 @@ static TextComponent prefixed(ComponentLike component) { .clickEvent(ClickEvent.openUrl(url)) ); + Args0 EDITOR_SOCKET_CONNECTED = () -> prefixed(translatable() + // "&bEditor window connected successfully." + .color(AQUA) + .key("luckperms.command.editor.socket.connected") + .append(FULL_STOP) + ); + + Args0 EDITOR_SOCKET_RECONNECTED = () -> prefixed(translatable() + // "&7Editor window reconnected successfully." + .color(GRAY) + .key("luckperms.command.editor.socket.reconnected") + .append(FULL_STOP) + ); + + Args0 EDITOR_SOCKET_CHANGES_RECEIVED = () -> prefixed(translatable() + // "&7Changes have been received from the connected web editor session." + .color(GRAY) + .key("luckperms.command.editor.socket.changes-received") + .append(FULL_STOP) + ); + + 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!" + // "&7If it was you, run &a/lp trusteditor aaaaa&7 to trust the session!" + prefixed(translatable() + .key("luckperms.command.editor.socket.untrusted") + .color(AQUA) + .append(FULL_STOP)), + prefixed(text() + .color(DARK_GRAY) + .append(OPEN_BRACKET) + .append(translatable() + .key("luckperms.command.editor.socket.untrusted.sessioninfo") + .color(GRAY) + .args( + text(nonce, WHITE), + text(browser, WHITE) + ) + ) + .append(CLOSE_BRACKET) + ), + prefixed(text() + .color(GRAY) + .apply(builder -> { + String command = "/" + cmdLabel + " trusteditor " + nonce; + if (console) { + builder.append(translatable() + .key("luckperms.command.editor.socket.untrusted.prompt.runcommand") + .args(text(command, GREEN)) + .build() + ); + } else { + builder.append(translatable() + .key("luckperms.command.editor.socket.untrusted.prompt.click") + .args(translatable() + .key("luckperms.command.editor.socket.untrusted.prompt.click.action") + .color(GREEN) + .clickEvent(ClickEvent.runCommand(command)) + ) + .build() + ); + } + })) + ); + + 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..." + prefixed(translatable() + .key("luckperms.command.editor.socket.trust.success") + .color(GREEN) + .append(FULL_STOP)), + prefixed(translatable() + .key("luckperms.command.editor.socket.trust.futureinfo") + .color(GRAY) + .append(FULL_STOP)), + prefixed(translatable() + .key("luckperms.command.editor.socket.trust.connecting") + .color(GRAY)) + ); + + Args0 EDITOR_SOCKET_TRUST_FAILURE = () -> prefixed(translatable() + // "&cUnable to trust the given session because the socket is closed, or because a different connection was established instead." + .color(RED) + .key("luckperms.command.editor.socket.trust.failure") + .append(FULL_STOP) + ); + Args2 EDITOR_HTTP_REQUEST_FAILURE = (code, message) -> prefixed(text() // "&cUnable to communicate with the editor. (response code &4{}&c, message='{}')" .color(RED) @@ -1301,7 +1570,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{}" @@ -1364,14 +1633,39 @@ static TextComponent prefixed(ComponentLike component) { .append(text(plugin.getStorage().getName(), WHITE)) ); - for (Map.Entry metaEntry : storageMeta.entrySet()) { + if (storageMeta.connected() != null) { + builder.append(newline()); + builder.append(prefixed(text() + .color(DARK_AQUA) + .append(text(" ")) + .append(translatable("luckperms.command.info.storage.meta.connected-key")) + .append(text(": ")) + .append(formatBoolean(storageMeta.connected())) + )); + } + + if (storageMeta.ping() != null) { + builder.append(newline()); + builder.append(prefixed(text() + .color(DARK_AQUA) + .append(text(" ")) + .append(translatable("luckperms.command.info.storage.meta.ping-key")) + .append(text(": ")) + .append(text(storageMeta.ping() + "ms", GREEN)) + )); + } + + if (storageMeta.sizeBytes() != null) { + DecimalFormat format = new DecimalFormat("#.##"); + String size = format.format(storageMeta.sizeBytes() / 1048576D) + "MB"; + builder.append(newline()); builder.append(prefixed(text() .color(DARK_AQUA) .append(text(" ")) - .append(metaEntry.getKey()) + .append(translatable("luckperms.command.info.storage.meta.file-size-key")) .append(text(": ")) - .append(metaEntry.getValue()) + .append(text(size, GREEN)) )); } })), @@ -1533,10 +1827,10 @@ static TextComponent prefixed(ComponentLike component) { .append(space()) .append(formatContextSetBracketed(node.getContexts(), empty())) .apply(builder -> { - String holderName = holder.getType() == HolderType.GROUP ? holder.getObjectName() : holder.getPlainDisplayName(); + 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()) @@ -1555,7 +1849,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()) @@ -1563,10 +1857,10 @@ static TextComponent prefixed(ComponentLike component) { .append(space()) .append(formatContextSetBracketed(node.getContexts(), empty())) .apply(builder -> { - String holderName = holder.getType() == HolderType.GROUP ? holder.getObjectName() : holder.getPlainDisplayName(); + 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()) @@ -1633,10 +1927,10 @@ static TextComponent prefixed(ComponentLike component) { .content(node.getGroupName()) .color(GREEN) .apply(builder -> { - String holderName = holder.getType() == HolderType.GROUP ? holder.getObjectName() : holder.getPlainDisplayName(); + 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()) @@ -1658,7 +1952,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()) @@ -1666,10 +1960,10 @@ static TextComponent prefixed(ComponentLike component) { .content(node.getGroupName()) .color(GREEN) .apply(builder -> { - String holderName = holder.getType() == HolderType.GROUP ? holder.getObjectName() : holder.getPlainDisplayName(); + 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)), @@ -1706,13 +2000,25 @@ static TextComponent prefixed(ComponentLike component) { .append(text(':')) ); - Args2 LIST_TRACKS_ENTRY = (name, path) -> text() - // "&a{}: {}" - .color(GREEN) - .append(text(name)) - .append(text(": ")) - .append(path) - .build(); + Args3 LIST_TRACKS_ENTRY = (name, contextSet, path) -> joinNewline( + // "&3> &a{}: {}" + // "&7 ({}&7)" + text() + .append(text('>', DARK_AQUA)) + .append(space()) + .append(text().color(GREEN) + .append(text(name)) + .append(text(": ")) + .append(formatContextSetBracketed(contextSet, empty())) + .build() + ), + text() + .color(GRAY) + .append(text(" ")) + .append(OPEN_BRACKET) + .append(path) + .append(CLOSE_BRACKET) + ); Args1 LIST_TRACKS_EMPTY = holder -> prefixed(translatable() // "&b{}&a is not on any tracks." @@ -1796,7 +2102,7 @@ static TextComponent prefixed(ComponentLike component) { ) ); - Args5 PERMISSION_CHECK_RESULT = (permission, result, processor, cause, context) -> join(newline(), + Args5 PERMISSION_CHECK_RESULT = (permission, result, processor, causeNode, context) -> joinNewline( // &aPermission check for &b{}&a: // &3Result: {} // &3Processor: &f{} @@ -1831,10 +2137,22 @@ static TextComponent prefixed(ComponentLike component) { .append(translatable("luckperms.command.generic.permission.check.result.cause-key")) .append(text(": ")) .apply(builder -> { - if (cause == null) { + if (causeNode == null) { builder.append(translatable("luckperms.command.misc.none", AQUA)); } else { - builder.append(text(cause, WHITE)); + String origin = causeNode.getMetadata(InheritanceOriginMetadata.KEY) + .map(o -> o.getOrigin().getName()).orElse("?"); + + builder.append(translatable() + .key("luckperms.command.generic.permission.check.info.directly") + .color(GRAY) + .args( + text().color(AQUA).content(origin), + text(causeNode.getKey(), AQUA), + formatBoolean(causeNode.getValue()), + formatContextSet(causeNode.getContexts()) + ) + ); } })), prefixed(text() @@ -1959,13 +2277,28 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); - Args0 PERMISSION_INVALID_ENTRY_EMPTY = () -> prefixed(translatable() - // "&cThe empty string is not a valid permission." - .key("luckperms.command.misc.permission-invalid-empty") + Args1 INVALID_INPUT_EMPTY = s -> prefixed(translatable() + // "&cThe empty string is not a valid {s}." + .key("luckperms.command.misc.invalid-input-empty-" + s) .color(RED) .append(FULL_STOP) ); + // Predefined shorthands for the above message + Args0 INVALID_PERMISSION_EMPTY = () -> INVALID_INPUT_EMPTY.build("permission"); + Args0 INVALID_META_KEY_EMPTY = () -> INVALID_INPUT_EMPTY.build("meta-key"); + Args0 INVALID_DISPLAY_NAME_EMPTY = () -> INVALID_INPUT_EMPTY.build("display-name"); + + Args2 SHORTHAND_PARSE_ERROR = (key, error) -> prefixed(translatable() + // "&cWarning: Permission &f{}&c could not be parsed as shorthand: &4{}&c" + .key("luckperms.command.misc.shorthand-parse-error") + .color(RED) + .args( + text(key, WHITE), + text(error, DARK_RED) + ) + ); + Args3 SET_INHERIT_SUCCESS = (holder, parent, context) -> prefixed(translatable() // "&b{}&a now inherits permissions from &b{}&a in context {}&a." .key("luckperms.command.generic.parent.add") @@ -2255,7 +2588,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()) @@ -2330,7 +2663,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()) @@ -2626,6 +2959,13 @@ static TextComponent prefixed(ComponentLike component) { .append(FULL_STOP) ); + Args0 BULK_UPDATE_DISABLED = () -> prefixed(translatable() + // "&cBulk update functionality is disabled in the configuration file." + .key("luckperms.command.bulkupdate.disabled") + .color(RED) + .append(FULL_STOP) + ); + Args0 BULK_UPDATE_MUST_USE_CONSOLE = () -> prefixed(translatable() // "&cThe bulk update command can only be used from the console." .key("luckperms.command.bulkupdate.must-use-console") @@ -2719,7 +3059,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{}" @@ -2801,7 +3141,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() @@ -2845,7 +3185,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)" @@ -2879,7 +3219,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: {}" @@ -2966,7 +3306,7 @@ static TextComponent prefixed(ComponentLike component) { .build() ) .collect(Collectors.toList()); - builder.append(join(space(), entries)); + builder.append(join(JoinConfiguration.separator(space()), entries)); } })) ); @@ -2993,7 +3333,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)) @@ -3122,7 +3462,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() @@ -3176,7 +3516,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() @@ -3193,7 +3533,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{}" @@ -3218,7 +3558,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: {}" @@ -3291,7 +3631,7 @@ static TextComponent prefixed(ComponentLike component) { .build() ) .collect(Collectors.toList()); - builder.append(join(space(), entries)); + builder.append(join(JoinConfiguration.separator(space()), entries)); } })) ); @@ -3347,7 +3687,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() @@ -3453,7 +3793,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() @@ -3466,6 +3806,7 @@ static TextComponent prefixed(ComponentLike component) { .color(GRAY) .key("luckperms.duration.since") .args(DurationFormatter.CONCISE_LOW_ACCURACY.format(action.getDurationSince())) + .hoverEvent(HoverEvent.showText(text().append(translatable("luckperms.duration.date", GRAY)).append(text(": ", GRAY)).append(text(DATE_FORMAT.format(action.getTimestamp()), AQUA)))) ) .append(CLOSE_BRACKET) ) @@ -3480,7 +3821,7 @@ static TextComponent prefixed(ComponentLike component) { .append(text() .color(DARK_GRAY) .append(text('[')) - .append(text(LoggedAction.getTypeCharacter(action.getTarget().getType()), GREEN)) + .append(text(LoggedAction.getTypeString(action.getTarget().getType()), GREEN)) .append(text(']')) ) .append(space()) @@ -3713,7 +4054,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" @@ -3726,7 +4067,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." @@ -3881,8 +4223,71 @@ static TextComponent prefixed(ComponentLike component) { ); static Component formatColoredValue(String value) { - return LegacyComponentSerializer.legacyAmpersand().deserialize(value).toBuilder() - .hoverEvent(HoverEvent.showText(text(value, WHITE))) + 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 formatNodeKeyWithType(Node node) { + Component type, key; + + if (node instanceof InheritanceNode) { + type = translatable("luckperms.command.misc.node.inheritance", BLUE); + key = text(((InheritanceNode) node).getGroupName()); + } else if (node instanceof PrefixNode || node instanceof SuffixNode) { + type = node instanceof PrefixNode + ? translatable("luckperms.command.misc.node.prefix", GREEN) + : translatable("luckperms.command.misc.node.suffix", YELLOW); + key = text() + .append(text() + .color(WHITE) + .append(text('\'')) + .append(formatColoredValue(((ChatMetaNode) node).getMetaValue())) + .append(text('\'')) + ) + .append(space()) + .append(text() + .color(GRAY) + .append(OPEN_BRACKET) + .append(translatable("luckperms.command.misc.priority-label", text(((ChatMetaNode) node).getPriority()))) + .append(CLOSE_BRACKET) + ) + .build(); + } else if (node instanceof MetaNode) { + type = translatable("luckperms.command.misc.node.meta", RED); + key = text() + .append(text(((MetaNode) node).getMetaKey())) + .append(text(" = ", GRAY)) + .append(text(((MetaNode) node).getMetaValue())) + .build(); + } else if (node instanceof WeightNode) { + type = translatable("luckperms.command.misc.node.weight", AQUA); + key = text(((WeightNode) node).getWeight()); + } else if (node instanceof DisplayNameNode) { + type = translatable("luckperms.command.misc.node.displayname", DARK_PURPLE); + key = text(((DisplayNameNode) node).getDisplayName()); + } else { + type = translatable("luckperms.command.misc.node.permission", GRAY); + key = text(node.getKey()); + } + + return text() + .append(text() + .color(DARK_GRAY) + .append(text("[")) + .append(type) + .append(text("]:"))) + .append(space()) + .append(key) .build(); } @@ -4051,6 +4456,10 @@ static Component formatTristate(Tristate tristate) { } } + static Component joinNewline(final ComponentLike... components) { + return join(JoinConfiguration.newlines(), components); + } + interface Args0 { Component build(); diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/TranslationManager.java b/common/src/main/java/me/lucko/luckperms/common/locale/TranslationManager.java index f14f83efb..2ceb2ba66 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/TranslationManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/TranslationManager.java @@ -26,16 +26,14 @@ package me.lucko.luckperms.common.locale; import com.google.common.collect.Maps; - import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - +import me.lucko.luckperms.common.util.MoreFiles; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.translation.GlobalTranslator; import net.kyori.adventure.translation.TranslationRegistry; import net.kyori.adventure.translation.Translator; import net.kyori.adventure.util.UTF8ResourceBundleControl; - import org.checkerframework.checker.nullness.qual.Nullable; import java.io.BufferedReader; @@ -60,19 +58,39 @@ public class TranslationManager { public static final Locale DEFAULT_LOCALE = Locale.ENGLISH; private final LuckPermsPlugin plugin; - private final Path translationsDirectory; private final Set installed = ConcurrentHashMap.newKeySet(); private TranslationRegistry registry; + private final Path translationsDirectory; + private final Path repositoryTranslationsDirectory; + private final Path customTranslationsDirectory; + public TranslationManager(LuckPermsPlugin plugin) { this.plugin = plugin; this.translationsDirectory = this.plugin.getBootstrap().getConfigDirectory().resolve("translations"); + this.repositoryTranslationsDirectory = this.translationsDirectory.resolve("repository"); + this.customTranslationsDirectory = this.translationsDirectory.resolve("custom"); + + try { + MoreFiles.createDirectoriesIfNotExists(this.repositoryTranslationsDirectory); + MoreFiles.createDirectoriesIfNotExists(this.customTranslationsDirectory); + } catch (IOException e) { + // ignore + } } public Path getTranslationsDirectory() { return this.translationsDirectory; } + public Path getRepositoryTranslationsDirectory() { + return this.repositoryTranslationsDirectory; + } + + public Path getRepositoryStatusFile() { + return this.repositoryTranslationsDirectory.resolve("status.json"); + } + public Set getInstalledLocales() { return Collections.unmodifiableSet(this.installed); } @@ -80,7 +98,7 @@ public Set getInstalledLocales() { public void reload() { // remove any previous registry if (this.registry != null) { - GlobalTranslator.get().removeSource(this.registry); + GlobalTranslator.translator().removeSource(this.registry); this.installed.clear(); } @@ -89,45 +107,56 @@ public void reload() { this.registry.defaultLocale(DEFAULT_LOCALE); // load custom translations first, then the base (built-in) translations after. - loadCustom(); - loadBase(); + loadFromFileSystem(this.customTranslationsDirectory, false); + loadFromFileSystem(this.repositoryTranslationsDirectory, true); + loadFromResourceBundle(); // register it to the global source, so our translations can be picked up by adventure-platform - GlobalTranslator.get().addSource(this.registry); + GlobalTranslator.translator().addSource(this.registry); } /** * Loads the base (English) translations from the jar file. */ - private void loadBase() { + private void loadFromResourceBundle() { ResourceBundle bundle = ResourceBundle.getBundle("luckperms", DEFAULT_LOCALE, UTF8ResourceBundleControl.get()); try { this.registry.registerAll(DEFAULT_LOCALE, bundle, false); } catch (IllegalArgumentException e) { - this.plugin.getLogger().warn("Error loading default locale file", e); + if (!isAdventureDuplicatesException(e)) { + this.plugin.getLogger().warn("Error loading default locale file", e); + } } } + public static boolean isTranslationFile(Path path) { + return path.getFileName().toString().endsWith(".properties"); + } + /** * Loads custom translations (in any language) from the plugin configuration folder. */ - public void loadCustom() { + public void loadFromFileSystem(Path directory, boolean suppressDuplicatesError) { List translationFiles; - try (Stream stream = Files.list(this.translationsDirectory)) { - translationFiles = stream.filter(path -> path.getFileName().toString().endsWith(".properties")).collect(Collectors.toList()); + try (Stream stream = Files.list(directory)) { + translationFiles = stream.filter(TranslationManager::isTranslationFile).collect(Collectors.toList()); } catch (IOException e) { translationFiles = Collections.emptyList(); } + if (translationFiles.isEmpty()) { + return; + } + Map loaded = new HashMap<>(); for (Path translationFile : translationFiles) { try { - Map.Entry result = loadCustomTranslationFile(translationFile); - if (result != null) { - loaded.put(result.getKey(), result.getValue()); - } + Map.Entry result = loadTranslationFile(translationFile); + loaded.put(result.getKey(), result.getValue()); } catch (Exception e) { - this.plugin.getLogger().warn("Error loading locale file: " + translationFile.getFileName(), e); + if (!suppressDuplicatesError || !isAdventureDuplicatesException(e)) { + this.plugin.getLogger().warn("Error loading locale file: " + translationFile.getFileName(), e); + } } } @@ -135,27 +164,27 @@ public void loadCustom() { loaded.forEach((locale, bundle) -> { Locale localeWithoutCountry = new Locale(locale.getLanguage()); if (!locale.equals(localeWithoutCountry) && !localeWithoutCountry.equals(DEFAULT_LOCALE) && this.installed.add(localeWithoutCountry)) { - this.registry.registerAll(localeWithoutCountry, bundle, false); + try { + this.registry.registerAll(localeWithoutCountry, bundle, false); + } catch (IllegalArgumentException e) { + // ignore + } } }); } - private Map.Entry loadCustomTranslationFile(Path translationFile) { + private Map.Entry loadTranslationFile(Path translationFile) throws IOException { String fileName = translationFile.getFileName().toString(); String localeString = fileName.substring(0, fileName.length() - ".properties".length()); Locale locale = parseLocale(localeString); if (locale == null) { - this.plugin.getLogger().warn("Unknown locale '" + localeString + "' - unable to register."); - return null; + throw new IllegalStateException("Unknown locale '" + localeString + "' - unable to register."); } PropertyResourceBundle bundle; try (BufferedReader reader = Files.newBufferedReader(translationFile, StandardCharsets.UTF_8)) { bundle = new PropertyResourceBundle(reader); - } catch(IOException e) { - this.plugin.getLogger().warn("Error loading locale file: " + localeString, e); - return null; } this.registry.registerAll(locale, bundle, false); @@ -163,6 +192,11 @@ private Map.Entry loadCustomTranslationFile(Path transla return Maps.immutableEntry(locale, bundle); } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private static boolean isAdventureDuplicatesException(Exception e) { + return e instanceof IllegalArgumentException && (e.getMessage().startsWith("Invalid key") || e.getMessage().startsWith("Translation already exists")); + } + public static Component render(Component component) { return render(component, Locale.getDefault()); } diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/TranslationRepository.java b/common/src/main/java/me/lucko/luckperms/common/locale/TranslationRepository.java index b71a98bc7..c350e7a9f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/TranslationRepository.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/TranslationRepository.java @@ -28,19 +28,15 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; - import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.http.UnsuccessfulRequestException; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.util.MoreFiles; import me.lucko.luckperms.common.util.gson.GsonProvider; - -import org.checkerframework.checker.nullness.qual.Nullable; - import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; +import org.checkerframework.checker.nullness.qual.Nullable; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -59,6 +55,7 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; public class TranslationRepository { private static final String TRANSLATIONS_INFO_ENDPOINT = "https://metadata.luckperms.net/data/translations"; @@ -92,6 +89,9 @@ public void scheduleRefresh() { } this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + // cleanup old translation files + clearDirectory(this.plugin.getTranslationManager().getTranslationsDirectory(), Files::isRegularFile); + try { refresh(); } catch (Exception e) { @@ -101,34 +101,14 @@ public void scheduleRefresh() { } private void refresh() throws Exception { - Path translationsDirectory = this.plugin.getTranslationManager().getTranslationsDirectory(); - try { - MoreFiles.createDirectoriesIfNotExists(translationsDirectory); - } catch (IOException e) { - // ignore - } - - long lastRefresh = 0L; - - Path repoStatusFile = translationsDirectory.resolve("repository.json"); - if (Files.exists(repoStatusFile)) { - try (BufferedReader reader = Files.newBufferedReader(repoStatusFile, StandardCharsets.UTF_8)) { - JsonObject status = GsonProvider.normal().fromJson(reader, JsonObject.class); - if (status.has("lastRefresh")) { - lastRefresh = status.get("lastRefresh").getAsLong(); - } - } catch (Exception e) { - // ignore - } - } - + long lastRefresh = readLastRefreshTime(); long timeSinceLastRefresh = System.currentTimeMillis() - lastRefresh; + if (timeSinceLastRefresh <= CACHE_MAX_AGE) { return; } MetadataResponse metadata = getTranslationsMetadata(); - if (timeSinceLastRefresh <= metadata.cacheMaxAge) { return; } @@ -137,6 +117,22 @@ private void refresh() throws Exception { downloadAndInstallTranslations(metadata.languages, null, true); } + private void clearDirectory(Path directory, Predicate predicate) { + try { + Files.list(directory) + .filter(predicate) + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + // ignore + } + }); + } catch (IOException e) { + // ignore + } + } + /** * Downloads and installs translations for the given languages. * @@ -146,13 +142,10 @@ private void refresh() throws Exception { */ public void downloadAndInstallTranslations(List languages, @Nullable Sender sender, boolean updateStatus) { TranslationManager manager = this.plugin.getTranslationManager(); - Path translationsDirectory = manager.getTranslationsDirectory(); + Path translationsDirectory = manager.getRepositoryTranslationsDirectory(); - try { - MoreFiles.createDirectoriesIfNotExists(translationsDirectory); - } catch (IOException e) { - // ignore - } + // clear existing translations + clearDirectory(translationsDirectory, TranslationManager::isTranslationFile); for (LanguageInfo language : languages) { if (sender != null) { @@ -185,18 +178,39 @@ public void downloadAndInstallTranslations(List languages, @Nullab } if (updateStatus) { - // update status file - Path repoStatusFile = translationsDirectory.resolve("repository.json"); - try (BufferedWriter writer = Files.newBufferedWriter(repoStatusFile, StandardCharsets.UTF_8)) { - JsonObject status = new JsonObject(); - status.add("lastRefresh", new JsonPrimitive(System.currentTimeMillis())); - GsonProvider.prettyPrinting().toJson(status, writer); - } catch (IOException e) { + writeLastRefreshTime(); + } + + manager.reload(); + } + + private void writeLastRefreshTime() { + Path statusFile = this.plugin.getTranslationManager().getRepositoryStatusFile(); + + try (BufferedWriter writer = Files.newBufferedWriter(statusFile, StandardCharsets.UTF_8)) { + JsonObject status = new JsonObject(); + status.add("lastRefresh", new JsonPrimitive(System.currentTimeMillis())); + GsonProvider.prettyPrinting().toJson(status, writer); + } catch (IOException e) { + // ignore + } + } + + private long readLastRefreshTime() { + Path statusFile = this.plugin.getTranslationManager().getRepositoryStatusFile(); + + if (Files.exists(statusFile)) { + try (BufferedReader reader = Files.newBufferedReader(statusFile, StandardCharsets.UTF_8)) { + JsonObject status = GsonProvider.normal().fromJson(reader, JsonObject.class); + if (status.has("lastRefresh")) { + return status.get("lastRefresh").getAsLong(); + } + } catch (Exception e) { // ignore } } - this.plugin.getTranslationManager().reload(); + return 0L; } private MetadataResponse getTranslationsMetadata() throws IOException, UnsuccessfulRequestException { diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java b/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java index 7f028d02a..a54a2cc14 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/InternalMessagingService.java @@ -27,11 +27,12 @@ import me.lucko.luckperms.common.cache.BufferedRequest; import me.lucko.luckperms.common.model.User; - import net.luckperms.api.actionlog.Action; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; +import java.util.concurrent.CompletableFuture; + public interface InternalMessagingService { /** @@ -61,20 +62,28 @@ public interface InternalMessagingService { * Uses the messaging service to inform other servers about a general * change. */ - void pushUpdate(); + CompletableFuture pushUpdate(); /** * Pushes an update for a specific user. * * @param user the user */ - void pushUserUpdate(User user); + CompletableFuture pushUserUpdate(User user); /** * Pushes a log entry to connected servers. * * @param logEntry the log entry */ - void pushLog(Action logEntry); + CompletableFuture pushLog(Action logEntry); + + /** + * Pushes a custom payload to connected servers. + * + * @param channelId the channel id + * @param payload the payload + */ + CompletableFuture pushCustomPayload(String channelId, String payload); } diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java b/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java index af7465c54..4322c3204 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/LuckPermsMessagingService.java @@ -27,37 +27,39 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.cache.BufferedRequest; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.messaging.message.ActionLogMessageImpl; +import me.lucko.luckperms.common.messaging.message.CustomMessageImpl; import me.lucko.luckperms.common.messaging.message.UpdateMessageImpl; import me.lucko.luckperms.common.messaging.message.UserUpdateMessageImpl; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.util.AsyncInterface; import me.lucko.luckperms.common.util.ExpiringSet; import me.lucko.luckperms.common.util.gson.GsonProvider; import me.lucko.luckperms.common.util.gson.JObject; - import net.luckperms.api.actionlog.Action; +import net.luckperms.api.event.sync.SyncType; import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; import net.luckperms.api.messenger.message.Message; import net.luckperms.api.messenger.message.type.ActionLogMessage; +import net.luckperms.api.messenger.message.type.CustomMessage; import net.luckperms.api.messenger.message.type.UpdateMessage; import net.luckperms.api.messenger.message.type.UserUpdateMessage; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -public class LuckPermsMessagingService implements InternalMessagingService, IncomingMessageConsumer { +public class LuckPermsMessagingService extends AsyncInterface implements InternalMessagingService, IncomingMessageConsumer { private final LuckPermsPlugin plugin; private final Set receivedMessages; private final PushUpdateBuffer updateBuffer; @@ -66,13 +68,14 @@ public class LuckPermsMessagingService implements InternalMessagingService, Inco private final Messenger messenger; public LuckPermsMessagingService(LuckPermsPlugin plugin, MessengerProvider messengerProvider) { + super(plugin); this.plugin = plugin; this.messengerProvider = messengerProvider; this.messenger = messengerProvider.obtain(this); Objects.requireNonNull(this.messenger, "messenger"); - this.receivedMessages = new ExpiringSet<>(1, TimeUnit.HOURS); + this.receivedMessages = ExpiringSet.newExpiringSet(5, TimeUnit.MINUTES); this.updateBuffer = new PushUpdateBuffer(plugin); } @@ -108,8 +111,8 @@ private UUID generatePingId() { } @Override - public void pushUpdate() { - this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + public CompletableFuture pushUpdate() { + return future(() -> { UUID requestId = generatePingId(); this.plugin.getLogger().info("[Messaging] Sending ping with id: " + requestId); this.messenger.sendOutgoingMessage(new UpdateMessageImpl(requestId)); @@ -117,8 +120,8 @@ public void pushUpdate() { } @Override - public void pushUserUpdate(User user) { - this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + public CompletableFuture pushUserUpdate(User user) { + return future(() -> { UUID requestId = generatePingId(); this.plugin.getLogger().info("[Messaging] Sending user ping for '" + user.getPlainDisplayName() + "' with id: " + requestId); this.messenger.sendOutgoingMessage(new UserUpdateMessageImpl(requestId, user.getUniqueId())); @@ -126,8 +129,8 @@ public void pushUserUpdate(User user) { } @Override - public void pushLog(Action logEntry) { - this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + public CompletableFuture pushLog(Action logEntry) { + return future(() -> { UUID requestId = generatePingId(); if (this.plugin.getEventDispatcher().dispatchLogNetworkPublish(!this.plugin.getConfiguration().get(ConfigKeys.PUSH_LOG_ENTRIES), requestId, logEntry)) { @@ -139,6 +142,14 @@ public void pushLog(Action logEntry) { }); } + @Override + public CompletableFuture pushCustomPayload(String channelId, String payload) { + return future(() -> { + UUID requestId = generatePingId(); + this.messenger.sendOutgoingMessage(new CustomMessageImpl(requestId, channelId, payload)); + }); + } + @Override public boolean consumeIncomingMessage(@NonNull Message message) { Objects.requireNonNull(message, "message"); @@ -150,7 +161,8 @@ public boolean consumeIncomingMessage(@NonNull Message message) { // determine if the message can be handled by us boolean valid = message instanceof UpdateMessage || message instanceof UserUpdateMessage || - message instanceof ActionLogMessage; + message instanceof ActionLogMessage || + message instanceof CustomMessage; // instead of throwing an exception here, just return false // it means an instance of LP can gracefully handle messages it doesn't @@ -165,11 +177,21 @@ public boolean consumeIncomingMessage(@NonNull Message message) { @Override public boolean consumeIncomingMessageAsString(@NonNull String encodedString) { + try { + return consumeIncomingMessageAsString0(encodedString); + } catch (Exception e) { + this.plugin.getLogger().warn("Unable to decode incoming messaging service message: '" + encodedString + "'", e); + return false; + } + } + + private boolean consumeIncomingMessageAsString0(@NonNull String encodedString) { Objects.requireNonNull(encodedString, "encodedString"); - JsonObject decodedObject = GsonProvider.normal().fromJson(encodedString, JsonObject.class).getAsJsonObject(); + JsonObject parsed = Objects.requireNonNull(GsonProvider.normal().fromJson(encodedString, JsonObject.class), "parsed"); + JsonObject json = parsed.getAsJsonObject(); // extract id - JsonElement idElement = decodedObject.get("id"); + JsonElement idElement = json.get("id"); if (idElement == null) { throw new IllegalStateException("Incoming message has no id argument: " + encodedString); } @@ -181,14 +203,14 @@ public boolean consumeIncomingMessageAsString(@NonNull String encodedString) { } // extract type - JsonElement typeElement = decodedObject.get("type"); + JsonElement typeElement = json.get("type"); if (typeElement == null) { throw new IllegalStateException("Incoming message has no type argument: " + encodedString); } String type = typeElement.getAsString(); // extract content - @Nullable JsonElement content = decodedObject.get("content"); + @Nullable JsonElement content = json.get("content"); // decode message Message decoded; @@ -202,6 +224,9 @@ public boolean consumeIncomingMessageAsString(@NonNull String encodedString) { case ActionLogMessageImpl.TYPE: decoded = ActionLogMessageImpl.decode(content, id); break; + case CustomMessageImpl.TYPE: + decoded = CustomMessageImpl.decode(content, id); + break; default: // gracefully return if we just don't recognise the type return false; @@ -229,34 +254,46 @@ public static String encodeMessageAsString(String type, UUID id, @Nullable JsonE private void processIncomingMessage(Message message) { if (message instanceof UpdateMessage) { UpdateMessage msg = (UpdateMessage) message; + UUID msgId = msg.getId(); - this.plugin.getLogger().info("[Messaging] Received update ping with id: " + msg.getId()); - - if (this.plugin.getEventDispatcher().dispatchNetworkPreSync(false, msg.getId())) { + if (this.plugin.getEventDispatcher().dispatchNetworkPreSync(false, msgId, SyncType.FULL, null)) { return; } - this.plugin.getSyncTaskBuffer().request(); + this.plugin.getLogger().info("[Messaging] Received update ping with id: " + msgId); + this.plugin.getSyncTaskBuffer().request() + .thenRunAsync(() -> this.plugin.getEventDispatcher().dispatchNetworkPostSync(msgId, SyncType.FULL, true, null)); + } else if (message instanceof UserUpdateMessage) { UserUpdateMessage msg = (UserUpdateMessage) message; + UUID msgId = msg.getId(); + UUID userUniqueId = msg.getUserUniqueId(); - User user = this.plugin.getUserManager().getIfLoaded(msg.getUserUniqueId()); - if (user == null) { + if (this.plugin.getEventDispatcher().dispatchNetworkPreSync(false, msgId, SyncType.SPECIFIC_USER, userUniqueId)) { return; } - this.plugin.getLogger().info("[Messaging] Received user update ping for '" + user.getPlainDisplayName() + "' with id: " + msg.getId()); - - if (this.plugin.getEventDispatcher().dispatchNetworkPreSync(false, msg.getId())) { + User user = this.plugin.getUserManager().getIfLoaded(userUniqueId); + if (user == null) { + this.plugin.getEventDispatcher().dispatchNetworkPostSync(msgId, SyncType.SPECIFIC_USER, false, userUniqueId); return; } - this.plugin.getStorage().loadUser(user.getUniqueId(), null); + this.plugin.getLogger().info("[Messaging] Received user update ping for '" + user.getPlainDisplayName() + "' with id: " + msgId); + this.plugin.getStorage().loadUser(user.getUniqueId(), null) + .thenRunAsync(() -> this.plugin.getEventDispatcher().dispatchNetworkPostSync(msgId, SyncType.SPECIFIC_USER, true, userUniqueId)); + } else if (message instanceof ActionLogMessage) { ActionLogMessage msg = (ActionLogMessage) message; this.plugin.getEventDispatcher().dispatchLogReceive(msg.getId(), msg.getAction()); - this.plugin.getLogDispatcher().dispatchFromRemote((LoggedAction) msg.getAction()); + this.plugin.getLogDispatcher().broadcastFromRemote((LoggedAction) msg.getAction()); + + } else if (message instanceof CustomMessage) { + CustomMessage msg = (CustomMessage) message; + + this.plugin.getEventDispatcher().dispatchCustomMessageReceive(msg.getChannelId(), msg.getPayload()); + } else { throw new IllegalArgumentException("Unknown message type: " + message.getClass().getName()); } 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 8fa0d4f91..e9ec391a2 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 @@ -27,6 +27,8 @@ import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.LuckPermsConfiguration; +import me.lucko.luckperms.common.messaging.nats.NatsMessenger; +import me.lucko.luckperms.common.messaging.postgres.PostgresMessenger; import me.lucko.luckperms.common.messaging.rabbitmq.RabbitMQMessenger; import me.lucko.luckperms.common.messaging.redis.RedisMessenger; import me.lucko.luckperms.common.messaging.sql.SqlMessenger; @@ -35,13 +37,16 @@ import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MariaDbConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MySqlConnectionFactory; - +import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.PostgresConnectionFactory; import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; 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

    { private final P plugin; @@ -65,6 +70,8 @@ public final InternalMessagingService getInstance() { messagingType = "redis"; } else if (this.plugin.getConfiguration().get(ConfigKeys.RABBITMQ_ENABLED)) { messagingType = "rabbitmq"; + } else if (this.plugin.getConfiguration().get(ConfigKeys.NATS_ENABLED)) { + messagingType = "nats"; } else { for (StorageImplementation implementation : this.plugin.getStorage().getImplementations()) { if (implementation instanceof SqlStorage) { @@ -73,6 +80,10 @@ public final InternalMessagingService getInstance() { messagingType = "sql"; break; } + if (sql.getConnectionFactory() instanceof PostgresConnectionFactory) { + messagingType = "postgresql"; + break; + } } } } @@ -82,13 +93,16 @@ public final InternalMessagingService getInstance() { return null; } - this.plugin.getLogger().info("Loading messaging service... [" + messagingType.toUpperCase() + "]"); + if (messagingType.equals("custom")) { + this.plugin.getLogger().info("Messaging service is set to custom. No service is initialized at this stage yet."); + return null; + } + this.plugin.getLogger().info("Loading messaging service... [" + messagingType.toUpperCase(Locale.ROOT) + "]"); InternalMessagingService service = getServiceFor(messagingType); if (service != null) { return service; } - this.plugin.getLogger().warn("Messaging service '" + messagingType + "' not recognised."); return null; } @@ -104,6 +118,16 @@ protected InternalMessagingService getServiceFor(String messagingType) { } else { this.plugin.getLogger().warn("Messaging Service was set to redis, but redis is not enabled!"); } + } else if (messagingType.equals("nats")) { + if (this.plugin.getConfiguration().get(ConfigKeys.NATS_ENABLED)) { + try { + return new LuckPermsMessagingService(this.plugin, new NatsMesengerProvider()); + } catch (Exception e) { + getPlugin().getLogger().severe("Exception occurred whilst enabling Nats messaging service", e); + } + } else { + this.plugin.getLogger().warn("Messaging Service was set to nats, but nats is not enabled!"); + } } else if (messagingType.equals("rabbitmq")) { if (this.plugin.getConfiguration().get(ConfigKeys.RABBITMQ_ENABLED)) { try { @@ -120,11 +144,49 @@ protected InternalMessagingService getServiceFor(String messagingType) { } catch (Exception e) { getPlugin().getLogger().severe("Exception occurred whilst enabling SQL messaging service", e); } + } else if (messagingType.equals("postgresql")) { + try { + return new LuckPermsMessagingService(this.plugin, new PostgresMessengerProvider()); + } catch (Exception e) { + getPlugin().getLogger().severe("Exception occurred whilst enabling Postgres messaging service", e); + } } return null; } + private class NatsMesengerProvider implements MessengerProvider { + + @Override + public @NonNull String getName() { + return "Nats"; + } + + @Override + public @NonNull Messenger obtain(@NonNull IncomingMessageConsumer incomingMessageConsumer) { + NatsMessenger natsMessenger = new NatsMessenger(getPlugin(), incomingMessageConsumer); + + LuckPermsConfiguration configuration = getPlugin().getConfiguration(); + String address = configuration.get(ConfigKeys.NATS_ADDRESS); + String username = configuration.get(ConfigKeys.NATS_USERNAME); + String password = configuration.get(ConfigKeys.NATS_PASSWORD); + String token = configuration.get(ConfigKeys.NATS_TOKEN); + if (password.isEmpty()) { + password = null; + } + if (username.isEmpty()) { + username = null; + } + if (token.isEmpty()) { + token = null; + } + boolean ssl = configuration.get(ConfigKeys.NATS_SSL); + + natsMessenger.init(address, username, password, token, ssl); + return natsMessenger; + } + } + private class RedisMessengerProvider implements MessengerProvider { @Override @@ -138,13 +200,43 @@ 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()) { password = null; } + if (username.isEmpty()) { + username = null; + } boolean ssl = config.get(ConfigKeys.REDIS_SSL); - redis.init(address, password, ssl); + boolean sentinelEnabled = config.get(ConfigKeys.REDIS_SENTINEL_ENABLED); + if (sentinelEnabled) { + // redis sentinel + String masterName = config.get(ConfigKeys.REDIS_SENTINEL_MASTER); + List sentinelAddresses = config.get(ConfigKeys.REDIS_SENTINEL_ADDRESSES); + String sentinelUsername = config.get(ConfigKeys.REDIS_SENTINEL_USERNAME); + String sentinelPassword = config.get(ConfigKeys.REDIS_SENTINEL_PASSWORD); + if (sentinelUsername.isEmpty()) { + sentinelUsername = null; + } + if (sentinelPassword.isEmpty()) { + sentinelPassword = null; + } + redis.init(masterName, sentinelAddresses, username, password, ssl, sentinelUsername, sentinelPassword); + } else 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; } } @@ -196,4 +288,29 @@ private class SqlMessengerProvider implements MessengerProvider { } } + private class PostgresMessengerProvider implements MessengerProvider { + + @Override + public @NonNull String getName() { + return "PostgreSQL"; + } + + @Override + public @NonNull Messenger obtain(@NonNull IncomingMessageConsumer incomingMessageConsumer) { + for (StorageImplementation implementation : getPlugin().getStorage().getImplementations()) { + if (implementation instanceof SqlStorage) { + SqlStorage storage = (SqlStorage) implementation; + if (storage.getConnectionFactory() instanceof PostgresConnectionFactory) { + // found an implementation match! + PostgresMessenger messenger = new PostgresMessenger(getPlugin(), storage, incomingMessageConsumer); + messenger.init(); + return messenger; + } + } + } + + throw new IllegalStateException("Can't find a supported sql storage implementation"); + } + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/message/AbstractMessage.java b/common/src/main/java/me/lucko/luckperms/common/messaging/message/AbstractMessage.java index f2097b686..c37f8711f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/message/AbstractMessage.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/message/AbstractMessage.java @@ -27,7 +27,6 @@ import net.luckperms.api.messenger.message.Message; import net.luckperms.api.messenger.message.OutgoingMessage; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.UUID; diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/message/ActionLogMessageImpl.java b/common/src/main/java/me/lucko/luckperms/common/messaging/message/ActionLogMessageImpl.java index 33d4cc45e..db24d1122 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/message/ActionLogMessageImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/message/ActionLogMessageImpl.java @@ -26,13 +26,10 @@ package me.lucko.luckperms.common.messaging.message; import com.google.gson.JsonElement; - import me.lucko.luckperms.common.actionlog.ActionJsonSerializer; import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; - import net.luckperms.api.actionlog.Action; import net.luckperms.api.messenger.message.type.ActionLogMessage; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/message/CustomMessageImpl.java b/common/src/main/java/me/lucko/luckperms/common/messaging/message/CustomMessageImpl.java new file mode 100644 index 000000000..0265eb35a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/message/CustomMessageImpl.java @@ -0,0 +1,85 @@ +/* + * 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.messaging.message; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; +import me.lucko.luckperms.common.util.gson.JObject; +import net.luckperms.api.messenger.message.type.CustomMessage; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.UUID; + +public class CustomMessageImpl extends AbstractMessage implements CustomMessage { + public static final String TYPE = "custom"; + + public static CustomMessageImpl decode(@Nullable JsonElement content, UUID id) { + if (content == null) { + throw new IllegalStateException("Missing content"); + } + + JsonObject obj = content.getAsJsonObject(); + if (!obj.has("channelId")) { + throw new IllegalStateException("Incoming message has no 'channelId' argument: " + content); + } + if (!obj.has("payload")) { + throw new IllegalStateException("Incoming message has no 'payload' argument: " + content); + } + + String channelId = obj.get("channelId").getAsString(); + String payload = obj.get("payload").getAsString(); + + return new CustomMessageImpl(id, channelId, payload); + } + + private final String channelId; + private final String payload; + + public CustomMessageImpl(UUID id, String channelId, String payload) { + super(id); + this.channelId = channelId; + this.payload = payload; + } + + @Override + public @NonNull String getChannelId() { + return this.channelId; + } + + @Override + public @NonNull String getPayload() { + return this.payload; + } + + @Override + public @NonNull String asEncodedString() { + return LuckPermsMessagingService.encodeMessageAsString( + TYPE, getId(), new JObject().add("channelId", this.channelId).add("payload", this.payload).toJson() + ); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/message/UpdateMessageImpl.java b/common/src/main/java/me/lucko/luckperms/common/messaging/message/UpdateMessageImpl.java index 525d56210..95a0e34c4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/message/UpdateMessageImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/message/UpdateMessageImpl.java @@ -26,11 +26,8 @@ package me.lucko.luckperms.common.messaging.message; import com.google.gson.JsonElement; - import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; - import net.luckperms.api.messenger.message.type.UpdateMessage; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/message/UserUpdateMessageImpl.java b/common/src/main/java/me/lucko/luckperms/common/messaging/message/UserUpdateMessageImpl.java index 77953cc6f..96d66c4d7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/message/UserUpdateMessageImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/message/UserUpdateMessageImpl.java @@ -26,12 +26,9 @@ package me.lucko.luckperms.common.messaging.message; import com.google.gson.JsonElement; - import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; import me.lucko.luckperms.common.util.gson.JObject; - import net.luckperms.api.messenger.message.type.UserUpdateMessage; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/nats/NatsMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/nats/NatsMessenger.java new file mode 100644 index 000000000..9a9e7c690 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/nats/NatsMessenger.java @@ -0,0 +1,131 @@ +/* + * 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.messaging.nats; + +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import io.nats.client.Connection; +import io.nats.client.Dispatcher; +import io.nats.client.Message; +import io.nats.client.MessageHandler; +import io.nats.client.Nats; +import io.nats.client.Options; +import io.nats.client.Options.Builder; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.util.HostAndPort; +import me.lucko.luckperms.common.util.Throwing; +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.luckperms.api.messenger.message.OutgoingMessage; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.time.Duration; + +/** + * An implementation of Messenger for Nats messaging client. + */ +public class NatsMessenger implements Messenger { + + private static final String CHANNEL = "luckperms:update"; + + private final LuckPermsPlugin plugin; + private final IncomingMessageConsumer consumer; + private Connection connection; + private Dispatcher messageDispatcher; + + public NatsMessenger(LuckPermsPlugin plugin, IncomingMessageConsumer consumer) { + this.plugin = plugin; + this.consumer = consumer; + } + + @Override + public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { + ByteArrayDataOutput output = ByteStreams.newDataOutput(); + output.writeUTF(outgoingMessage.asEncodedString()); + this.connection.publish(CHANNEL, output.toByteArray()); + } + + public void init(String address, String username, String password, String token, boolean ssl) { + HostAndPort hostAndPort = new HostAndPort(address) + .requireBracketsForIPv6() + .withDefaultPort(Options.DEFAULT_PORT); + String host = hostAndPort.getHost(); + int port = hostAndPort.getPort(); + + this.connection = createConnection(builder -> { + builder.server("nats://" + host + ":" + port) + .reconnectWait(Duration.ofSeconds(5)) + .maxReconnects(Integer.MAX_VALUE) + .connectionName("LuckPerms"); + + if (username != null && password != null) { + builder.userInfo(username, password); + } + + if (token != null) { + builder.token(token.toCharArray()); + } + + if (ssl) { + builder.secure(); + } + }); + this.messageDispatcher = this.connection.createDispatcher(new Handler()).subscribe(CHANNEL); + } + + private Connection createConnection(Throwing.Consumer config) { + try { + Builder builder = new Builder(); + config.accept(builder); + return Nats.connect(builder.build()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void close() { + try { + this.connection.closeDispatcher(this.messageDispatcher); + this.connection.close(); + } catch (InterruptedException e) { + this.plugin.getLogger().warn("An error occurred during closing messenger.", e); + } + } + + private class Handler implements MessageHandler { + + @Override + public void onMessage(Message message) { + byte[] data = message.getData(); + ByteArrayDataInput input = ByteStreams.newDataInput(data); + String messageAsString = input.readUTF(); + + NatsMessenger.this.consumer.consumeIncomingMessageAsString(messageAsString); + } + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/pluginmsg/AbstractPluginMessageMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/pluginmsg/AbstractPluginMessageMessenger.java new file mode 100644 index 000000000..4cbf43850 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/pluginmsg/AbstractPluginMessageMessenger.java @@ -0,0 +1,78 @@ +/* + * 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.messaging.pluginmsg; + +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.luckperms.api.messenger.message.OutgoingMessage; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Abstract implementation of {@link Messenger} using Minecraft's + * 'plugin messaging channels' packet. + * + *

    The {@link OutgoingMessage#asEncodedString() encoded string} format + * is used to transmit messages. {@link java.io.DataOutput#writeUTF(String)} is + * used to encode the string into raw bytes.

    + */ +public abstract class AbstractPluginMessageMessenger implements Messenger { + + /** + * The identifier of the channel used by LuckPerms for all messages. + */ + public static final String CHANNEL = "luckperms:update"; + + /** + * The {@link IncomingMessageConsumer} used by the messenger. + */ + private final IncomingMessageConsumer consumer; + + protected AbstractPluginMessageMessenger(IncomingMessageConsumer consumer) { + this.consumer = consumer; + } + + @Override + public final void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { + ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput(); + dataOutput.writeUTF(outgoingMessage.asEncodedString()); + + byte[] buf = dataOutput.toByteArray(); + + sendOutgoingMessage(buf); + } + + protected abstract void sendOutgoingMessage(byte[] buf); + + protected boolean handleIncomingMessage(byte[] buf) { + ByteArrayDataInput dataInput = ByteStreams.newDataInput(buf); + String decodedString = dataInput.readUTF(); + return this.consumer.consumeIncomingMessageAsString(decodedString); + } + +} 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 new file mode 100644 index 000000000..a79a9b63f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/postgres/PostgresMessenger.java @@ -0,0 +1,185 @@ +/* + * 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.messaging.postgres; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; +import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; +import net.luckperms.api.messenger.IncomingMessageConsumer; +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.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * An implementation of {@link Messenger} using Postgres. + */ +public class PostgresMessenger implements Messenger { + + private static final String CHANNEL = "luckperms:update"; + + private final LuckPermsPlugin plugin; + private final SqlStorage sqlStorage; + private final IncomingMessageConsumer consumer; + + private NotificationListener listener; + private SchedulerTask checkConnectionTask; + + public PostgresMessenger(LuckPermsPlugin plugin, SqlStorage sqlStorage, IncomingMessageConsumer consumer) { + this.plugin = plugin; + this.sqlStorage = sqlStorage; + this.consumer = consumer; + } + + public void init() { + checkAndReopenConnection(true); + this.checkConnectionTask = this.plugin.getBootstrap().getScheduler().asyncRepeating(() -> checkAndReopenConnection(false), 5, TimeUnit.SECONDS); + } + + @Override + public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { + try (Connection connection = this.sqlStorage.getConnectionFactory().getConnection()) { + try (PreparedStatement ps = connection.prepareStatement("SELECT pg_notify(?, ?)")) { + ps.setString(1, CHANNEL); + ps.setString(2, outgoingMessage.asEncodedString()); + ps.execute(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void close() { + try { + this.checkConnectionTask.cancel(); + if (this.listener != null) { + this.listener.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Checks the connection, and re-opens it if necessary. + * + * @return true if the connection is now alive, false otherwise + */ + private boolean checkAndReopenConnection(boolean firstStartup) { + boolean listenerActive = this.listener != null && this.listener.isListening(); + if (listenerActive) { + return true; + } + + // (re)create + + if (!firstStartup) { + this.plugin.getLogger().warn("Postgres listen/notify connection dropped, trying to re-open the connection"); + } + + try { + this.listener = new NotificationListener(); + this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + this.listener.listenAndBind(); + if (!firstStartup) { + this.plugin.getLogger().info("Postgres listen/notify connection re-established"); + } + }); + return true; + } catch (Exception ignored) { + return false; + } + } + + private class NotificationListener implements AutoCloseable { + private static final int RECEIVE_TIMEOUT_MILLIS = 1000; + + 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 + "\""); + } + + 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.listeningThread.set(null); + } + } + + public boolean isListening() { + return this.listeningThread.get() != null; + } + + public void handleNotification(PGNotification notification) { + if (!CHANNEL.equals(notification.getName())) { + return; + } + PostgresMessenger.this.consumer.consumeIncomingMessageAsString(notification.getParameter()); + } + + @Override + public void close() { + 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/messaging/rabbitmq/RabbitMQMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/rabbitmq/RabbitMQMessenger.java index 6d84deb2c..22b62f805 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/rabbitmq/RabbitMQMessenger.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/rabbitmq/RabbitMQMessenger.java @@ -35,15 +35,16 @@ import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.DeliverCallback; import com.rabbitmq.client.Delivery; - import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - +import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; +import me.lucko.luckperms.common.util.HostAndPort; import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.message.OutgoingMessage; - import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.concurrent.TimeUnit; + /** * An implementation of {@link Messenger} using RabbitMQ. */ @@ -62,6 +63,7 @@ public class RabbitMQMessenger implements Messenger { private Connection connection; private Channel channel; private Subscription sub; + private SchedulerTask checkConnectionTask; public RabbitMQMessenger(LuckPermsPlugin plugin, IncomingMessageConsumer consumer) { this.plugin = plugin; @@ -69,9 +71,12 @@ public RabbitMQMessenger(LuckPermsPlugin plugin, IncomingMessageConsumer consume } public void init(String address, String virtualHost, String username, String password) { - String[] addressSplit = address.split(":"); - String host = addressSplit[0]; - int port = addressSplit.length > 1 ? Integer.parseInt(addressSplit[1]) : DEFAULT_PORT; + HostAndPort hostAndPort = new HostAndPort(address) + .requireBracketsForIPv6() + .withDefaultPort(DEFAULT_PORT); + + String host = hostAndPort.getHost(); + int port = hostAndPort.getPort(); this.connectionFactory = new ConnectionFactory(); this.connectionFactory.setHost(host); @@ -81,7 +86,8 @@ public void init(String address, String virtualHost, String username, String pas this.connectionFactory.setPassword(password); this.sub = new Subscription(); - this.plugin.getBootstrap().getScheduler().executeAsync(this.sub); + checkAndReopenConnection(true); + this.checkConnectionTask = this.plugin.getBootstrap().getScheduler().asyncRepeating(() -> checkAndReopenConnection(false), 5, TimeUnit.SECONDS); } @Override @@ -100,7 +106,7 @@ public void close() { try { this.channel.close(); this.connection.close(); - this.sub.isClosed = true; + this.checkConnectionTask.cancel(); } catch (Exception e) { e.printStackTrace(); } @@ -154,35 +160,23 @@ private boolean checkAndReopenConnection(boolean firstStartup) { this.plugin.getLogger().info("RabbitMQ pubsub connection re-established"); } return true; - } catch (Exception ignored) { - return false; - } - } - - private class Subscription implements Runnable, DeliverCallback { - private boolean isClosed = false; - - @Override - public void run() { - boolean firstStartup = true; - while (!Thread.interrupted() && !this.isClosed) { + } catch (Exception e) { + if (firstStartup) { + this.plugin.getLogger().warn("Unable to connect to RabbitMQ, waiting for 5 seconds then retrying...", e); try { - if (!checkAndReopenConnection(firstStartup)) { - // Sleep for 5 seconds to prevent massive spam in console - Thread.sleep(5000); - continue; - } - - // Check connection life every every 30 seconds - Thread.sleep(30_000); - } catch (InterruptedException e) { + Thread.sleep(5000); + } catch (InterruptedException ex) { Thread.currentThread().interrupt(); - } finally { - firstStartup = false; } + return checkAndReopenConnection(false); + } else { + this.plugin.getLogger().severe("Unable to connect to RabbitMQ", e); + return false; } } + } + private class Subscription implements DeliverCallback { @Override public void handle(String consumerTag, Delivery message) { try { 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 859a93ed4..e10d705c2 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 @@ -26,77 +26,124 @@ package me.lucko.luckperms.common.messaging.redis; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.messenger.IncomingMessageConsumer; 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.JedisPool; -import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPooled; import redis.clients.jedis.JedisPubSub; +import redis.clients.jedis.JedisSentineled; import redis.clients.jedis.Protocol; +import redis.clients.jedis.UnifiedJedis; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** * An implementation of {@link Messenger} using Redis. */ public class RedisMessenger implements Messenger { private static final String CHANNEL = "luckperms:update"; + private static final int SENTINEL_DEFAULT_PORT = 26379; private final LuckPermsPlugin plugin; private final IncomingMessageConsumer consumer; - private JedisPool jedisPool; - private Subscription sub; + private /* final */ UnifiedJedis jedis; + private /* final */ Subscription sub; + private boolean closing = false; public RedisMessenger(LuckPermsPlugin plugin, IncomingMessageConsumer consumer) { this.plugin = plugin; this.consumer = consumer; } - public void init(String address, String password, boolean ssl) { - String[] addressSplit = address.split(":"); - String host = addressSplit[0]; - int port = addressSplit.length > 1 ? Integer.parseInt(addressSplit[1]) : Protocol.DEFAULT_PORT; + 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))); + } - this.jedisPool = new JedisPool(new JedisPoolConfig(), host, port, Protocol.DEFAULT_TIMEOUT, password, ssl); + public void init(String masterName, List sentinelAddresses, String username, String password, boolean ssl, String sentinelUsername, String sentinelPassword) { + Set sentinels = sentinelAddresses.stream() + .map(addr -> parseAddress(addr, SENTINEL_DEFAULT_PORT)) + .collect(Collectors.toSet()); + this.init(new JedisSentineled(masterName, jedisConfig(username, password, ssl), sentinels, jedisConfig(sentinelUsername, sentinelPassword, ssl))); + } - this.sub = new Subscription(); + 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) { + return parseAddress(address, Protocol.DEFAULT_PORT); + } + + private static HostAndPort parseAddress(String address, int defaultPort) { + me.lucko.luckperms.common.util.HostAndPort hostAndPort = new me.lucko.luckperms.common.util.HostAndPort(address) + .requireBracketsForIPv6() + .withDefaultPort(defaultPort); + String host = hostAndPort.getHost(); + int port = hostAndPort.getPort(); + 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 wasBroken = false; - while (!Thread.interrupted() && !RedisMessenger.this.jedisPool.isClosed()) { - try (Jedis jedis = RedisMessenger.this.jedisPool.getResource()) { - if (wasBroken) { - RedisMessenger.this.plugin.getLogger().info("Redis pubsub connection re-established"); - wasBroken = false; + boolean first = true; + while (!this.messenger.closing && !Thread.interrupted() && this.isRedisAlive()) { + try { + if (first) { + first = false; + } else { + this.messenger.plugin.getLogger().info("Redis pubsub connection re-established"); } - jedis.subscribe(this, CHANNEL); + + this.messenger.jedis.subscribe(this, CHANNEL); // blocking call } catch (Exception e) { - wasBroken = true; - RedisMessenger.this.plugin.getLogger().warn("Redis pubsub connection dropped, trying to re-open the connection", e); + if (this.messenger.closing) { + return; + } + + this.messenger.plugin.getLogger().warn("Redis pubsub connection dropped, trying to re-open the connection", e); try { unsubscribe(); } catch (Exception ignored) { @@ -118,8 +165,21 @@ 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 if (jedis instanceof JedisSentineled) { + return true; + } else { + throw new RuntimeException("Unknown jedis type: " + jedis.getClass().getName()); + } + } + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/sql/AbstractSqlMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/sql/AbstractSqlMessenger.java index 875d9b4da..0f1bd6ef1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/sql/AbstractSqlMessenger.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/sql/AbstractSqlMessenger.java @@ -28,7 +28,6 @@ import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.message.OutgoingMessage; - import org.checkerframework.checker.nullness.qual.NonNull; import java.sql.Connection; @@ -60,7 +59,7 @@ protected AbstractSqlMessenger(IncomingMessageConsumer consumer) { public void init() throws SQLException { try (Connection c = getConnection()) { // init table - String createStatement = "CREATE TABLE IF NOT EXISTS `" + getTableName() + "` (`id` INT AUTO_INCREMENT NOT NULL, `time` TIMESTAMP NOT NULL, `msg` TEXT NOT NULL, PRIMARY KEY (`id`)) DEFAULT CHARSET = utf8mb4"; + String createStatement = "CREATE TABLE IF NOT EXISTS `" + getTableName() + "` (`id` INT AUTO_INCREMENT NOT NULL, `time` TIMESTAMP NOT NULL, `msg` TEXT NOT NULL, PRIMARY KEY (`id`), KEY (`time`)) DEFAULT CHARSET = utf8mb4"; try (Statement s = c.createStatement()) { try { s.execute(createStatement); @@ -74,6 +73,18 @@ public void init() throws SQLException { } } + // add index for time column if it doesn't already exist + try (PreparedStatement ps = c.prepareStatement("SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'time' LIMIT 1")) { + ps.setString(1, getTableName()); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + try (Statement s = c.createStatement()) { + s.execute("CREATE INDEX `time` ON `" + getTableName() + "` (`time`)"); + } + } + } + } + // pull last id try (PreparedStatement ps = c.prepareStatement("SELECT MAX(`id`) as `latest` FROM `" + getTableName() + "`")) { try (ResultSet rs = ps.executeQuery()) { @@ -113,7 +124,7 @@ public void pollMessages() { } try (Connection c = getConnection()) { - try (PreparedStatement ps = c.prepareStatement("SELECT `id`, `msg` FROM `" + getTableName() + "` WHERE `id` > ? AND (NOW() - `time` < 30)")) { + try (PreparedStatement ps = c.prepareStatement("SELECT `id`, `msg` FROM `" + getTableName() + "` WHERE `id` > ? AND `time` > (NOW() - INTERVAL 30 SECOND)")) { ps.setLong(1, this.lastId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { @@ -140,7 +151,7 @@ public void runHousekeeping() { } try (Connection c = getConnection()) { - try (PreparedStatement ps = c.prepareStatement("DELETE FROM `" + getTableName() + "` WHERE (NOW() - `time` > 60)")) { + try (PreparedStatement ps = c.prepareStatement("DELETE FROM `" + getTableName() + "` WHERE `time` < (NOW() - INTERVAL 60 SECOND)")) { ps.execute(); } } catch (SQLException e) { diff --git a/common/src/main/java/me/lucko/luckperms/common/messaging/sql/SqlMessenger.java b/common/src/main/java/me/lucko/luckperms/common/messaging/sql/SqlMessenger.java index 9ed7955ca..e49208d7e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/messaging/sql/SqlMessenger.java +++ b/common/src/main/java/me/lucko/luckperms/common/messaging/sql/SqlMessenger.java @@ -29,7 +29,6 @@ import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; - import net.luckperms.api.messenger.IncomingMessageConsumer; import java.sql.Connection; @@ -87,6 +86,6 @@ protected Connection getConnection() throws SQLException { @Override protected String getTableName() { - return this.sqlStorage.getStatementProcessor().apply("{prefix}messenger"); + return this.sqlStorage.getStatementProcessor().process("{prefix}messenger"); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/DemotionResults.java b/common/src/main/java/me/lucko/luckperms/common/model/DemotionResults.java index 3d1fb89cf..993edbd65 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/DemotionResults.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/DemotionResults.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.model; import net.luckperms.api.track.DemotionResult; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; diff --git a/common/src/main/java/me/lucko/luckperms/common/model/Group.java b/common/src/main/java/me/lucko/luckperms/common/model/Group.java index af1273472..c3db40f64 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/Group.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/Group.java @@ -28,20 +28,20 @@ import me.lucko.luckperms.common.api.implementation.ApiGroup; import me.lucko.luckperms.common.cache.Cache; import me.lucko.luckperms.common.cacheddata.GroupCachedDataManager; +import me.lucko.luckperms.common.cacheddata.result.IntegerResult; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.DisplayNameNode; +import net.luckperms.api.node.types.WeightNode; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Locale; import java.util.Optional; -import java.util.OptionalInt; public class Group extends PermissionHolder { private final ApiGroup apiProxy = new ApiGroup(this); @@ -54,7 +54,7 @@ public class Group extends PermissionHolder { /** * Caches the groups weight */ - private final Cache weightCache = new WeightCache(this); + private final Cache> weightCache = new WeightCache(this); /** * Caches the groups display name @@ -67,8 +67,8 @@ public class Group extends PermissionHolder { private final GroupCachedDataManager cachedData; public Group(String name, LuckPermsPlugin plugin) { - super(plugin); - this.name = name.toLowerCase(); + super(plugin, name.toLowerCase(Locale.ROOT)); + this.name = getIdentifier().getName(); this.cachedData = new GroupCachedDataManager(this); getPlugin().getEventDispatcher().dispatchGroupCacheLoad(this, this.cachedData); @@ -88,11 +88,6 @@ public String getName() { return this.name; } - @Override - public String getObjectName() { - return this.name; - } - public ApiGroup getApiProxy() { return this.apiProxy; } @@ -147,7 +142,7 @@ public Optional calculateDisplayName(QueryOptions queryOptions) { } @Override - public OptionalInt getWeight() { + public IntegerResult getWeightResult() { return this.weightCache.get(); } @@ -180,7 +175,7 @@ public String toString() { public class DisplayNameCache extends Cache> { @Override protected @NonNull Optional supply() { - return calculateDisplayName(getPlugin().getContextManager().getStaticQueryOptions()); + return calculateDisplayName(getQueryOptions()); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/HolderType.java b/common/src/main/java/me/lucko/luckperms/common/model/HolderType.java index 21bf3f1d7..a46e85f7b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/HolderType.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/HolderType.java @@ -25,12 +25,14 @@ package me.lucko.luckperms.common.model; +import java.util.Locale; + public enum HolderType { USER, GROUP; @Override public String toString() { - return name().toLowerCase(); + return name().toLowerCase(Locale.ROOT); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/InheritanceOrigin.java b/common/src/main/java/me/lucko/luckperms/common/model/InheritanceOrigin.java index d9acea639..3c368afe6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/InheritanceOrigin.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/InheritanceOrigin.java @@ -25,20 +25,46 @@ package me.lucko.luckperms.common.model; -import net.luckperms.api.model.PermissionHolder; +import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; - import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Objects; + public class InheritanceOrigin implements InheritanceOriginMetadata { - private final PermissionHolder.Identifier location; + private final PermissionHolderIdentifier origin; + private final DataType dataType; + + public InheritanceOrigin(PermissionHolderIdentifier origin, DataType dataType) { + this.origin = Objects.requireNonNull(origin, "origin"); + this.dataType = Objects.requireNonNull(dataType, "dataType"); + } + + @Override + public @NonNull PermissionHolderIdentifier getOrigin() { + return this.origin; + } - public InheritanceOrigin(PermissionHolder.Identifier location) { - this.location = location; + @Override + public @NonNull DataType getDataType() { + return this.dataType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof InheritanceOriginMetadata)) return false; + InheritanceOriginMetadata that = (InheritanceOriginMetadata) o; + return this.origin.equals(that.getOrigin()) && this.dataType == that.getDataType(); + } + + @Override + public int hashCode() { + return Objects.hash(this.origin, getDataType()); } @Override - public PermissionHolder.@NonNull Identifier getOrigin() { - return this.location; + public String toString() { + return this.origin + " (" + this.dataType + ")"; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java index 226771385..ca7b8b774 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolder.java @@ -26,12 +26,11 @@ package me.lucko.luckperms.common.model; import com.google.common.collect.Iterables; - import me.lucko.luckperms.common.cacheddata.HolderCachedDataManager; +import me.lucko.luckperms.common.cacheddata.result.IntegerResult; import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator; import me.lucko.luckperms.common.inheritance.InheritanceComparator; import me.lucko.luckperms.common.inheritance.InheritanceGraph; -import me.lucko.luckperms.common.model.nodemap.MutateResult; import me.lucko.luckperms.common.model.nodemap.NodeMap; import me.lucko.luckperms.common.model.nodemap.NodeMapMutable; import me.lucko.luckperms.common.model.nodemap.RecordedNodeMap; @@ -39,7 +38,7 @@ import me.lucko.luckperms.common.node.comparator.NodeWithContextComparator; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.query.DataSelector; - +import me.lucko.luckperms.common.util.Difference; import net.kyori.adventure.text.Component; import net.luckperms.api.context.ContextSet; import net.luckperms.api.model.data.DataMutateResult; @@ -49,11 +48,10 @@ import net.luckperms.api.node.NodeEqualityPredicate; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.node.types.WeightNode; import net.luckperms.api.query.Flag; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; - -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -63,6 +61,7 @@ import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.OptionalInt; import java.util.SortedSet; @@ -101,7 +100,7 @@ public abstract class PermissionHolder { /** * The holders identifier */ - private @MonotonicNonNull PermissionHolderIdentifier identifier; + private final PermissionHolderIdentifier identifier; /** * The holders persistent nodes. @@ -110,7 +109,7 @@ public abstract class PermissionHolder { * * @see #normalData() */ - private final RecordedNodeMap normalNodes = new RecordedNodeMap(new NodeMapMutable(this)); + private final RecordedNodeMap normalNodes; /** * The holders transient nodes. @@ -122,20 +121,24 @@ public abstract class PermissionHolder { * * @see #transientData() */ - private final NodeMap transientNodes = new NodeMapMutable(this); + private final NodeMap transientNodes; /** * Comparator used to ordering groups when calculating inheritance */ - private final Comparator inheritanceComparator = InheritanceComparator.getFor(this); + private final Comparator inheritanceComparator; /** * Creates a new instance * * @param plugin the plugin instance */ - protected PermissionHolder(LuckPermsPlugin plugin) { + protected PermissionHolder(LuckPermsPlugin plugin, String objectName) { this.plugin = plugin; + this.identifier = new PermissionHolderIdentifier(getType(), objectName); + this.normalNodes = new RecordedNodeMap(new NodeMapMutable(this, DataType.NORMAL)); + this.transientNodes = new NodeMapMutable(this, DataType.TRANSIENT); + this.inheritanceComparator = InheritanceComparator.getFor(this); } // getters @@ -168,22 +171,9 @@ public NodeMap transientData() { } public PermissionHolderIdentifier getIdentifier() { - if (this.identifier == null) { - this.identifier = new PermissionHolderIdentifier(getType(), getObjectName()); - } return this.identifier; } - /** - * Gets the unique name of this holder object. - * - *

    Used as a base for identifying permission holding objects. Also acts - * as a method for preventing circular inheritance issues.

    - * - * @return the object name - */ - public abstract String getObjectName(); - /** * Gets the formatted display name of this permission holder * (for use in commands, etc) @@ -232,8 +222,17 @@ public void loadNodesFromStorage(Iterable set) { invalidateCache(); } - public MutateResult setNodes(DataType type, Iterable set, boolean callEvent) { - MutateResult res = getData(type).setContent(set); + public Difference setNodes(DataType type, Iterable set, boolean callEvent) { + Difference res = getData(type).setContent(set); + invalidateCache(); + if (callEvent) { + getPlugin().getEventDispatcher().dispatchNodeChanges(this, type, res); + } + return res; + } + + public Difference setNodes(DataType type, Difference changes, boolean callEvent) { + Difference res = getData(type).applyChanges(changes); invalidateCache(); if (callEvent) { getPlugin().getEventDispatcher().dispatchNodeChanges(this, type, res); @@ -259,7 +258,7 @@ public List getOwnNodes(QueryOptions queryOptions) { } public SortedSet getOwnNodesSorted(QueryOptions queryOptions) { - SortedSet nodes = new TreeSet<>(NodeWithContextComparator.reverse()); + SortedSet nodes = new TreeSet<>(NodeWithContextComparator.descending()); for (DataType dataType : queryOrder(queryOptions)) { getData(dataType).copyTo(nodes, queryOptions); } @@ -302,7 +301,7 @@ public SortedSet resolveInheritedNodesSorted(QueryOptions queryOptions) { return getOwnNodesSorted(queryOptions); } - SortedSet nodes = new TreeSet<>(NodeWithContextComparator.reverse()); + SortedSet nodes = new TreeSet<>(NodeWithContextComparator.descending()); InheritanceGraph graph = this.plugin.getInheritanceGraphFactory().getGraph(queryOptions); for (PermissionHolder holder : graph.traverse(this)) { for (DataType dataType : holder.queryOrder(queryOptions)) { @@ -353,19 +352,19 @@ public List resolveInheritanceTree(QueryOptions queryOptions) { return (List) inheritanceTree; } - public > M exportPermissions(IntFunction mapFactory, QueryOptions queryOptions, boolean convertToLowercase, boolean resolveShorthand) { + public > M exportPermissions(IntFunction mapFactory, QueryOptions queryOptions, boolean convertToLowercase, boolean resolveShorthand) { List entries = resolveInheritedNodes(queryOptions); M map = mapFactory.apply(entries.size()); processExportedPermissions(map, entries, convertToLowercase, resolveShorthand); return map; } - private static void processExportedPermissions(Map accumulator, List entries, boolean convertToLowercase, boolean resolveShorthand) { + private static void processExportedPermissions(Map accumulator, List entries, boolean convertToLowercase, boolean resolveShorthand) { for (Node node : entries) { if (convertToLowercase) { - accumulator.putIfAbsent(node.getKey().toLowerCase(), node.getValue()); + accumulator.putIfAbsent(node.getKey().toLowerCase(Locale.ROOT), node); } else { - accumulator.putIfAbsent(node.getKey(), node.getValue()); + accumulator.putIfAbsent(node.getKey(), node); } } @@ -374,9 +373,9 @@ private static void processExportedPermissions(Map accumulator, Collection shorthand = node.resolveShorthand(); for (String s : shorthand) { if (convertToLowercase) { - accumulator.putIfAbsent(s.toLowerCase(), node.getValue()); + accumulator.putIfAbsent(s.toLowerCase(Locale.ROOT), node); } else { - accumulator.putIfAbsent(s, node.getValue()); + accumulator.putIfAbsent(s, node); } } } @@ -393,16 +392,16 @@ public MetaAccumulator accumulateMeta(MetaAccumulator accumulator, QueryOptions // accumulate nodes for (DataType dataType : holder.queryOrder(queryOptions)) { holder.getData(dataType).forEach(queryOptions, node -> { - if (node.getValue() && NodeType.META_OR_CHAT_META.matches(node)) { + if (NodeType.META_OR_CHAT_META.matches(node)) { accumulator.accumulateNode(node); } }); } // accumulate weight - OptionalInt w = holder.getWeight(); - if (w.isPresent()) { - accumulator.accumulateWeight(w.getAsInt()); + IntegerResult weight = holder.getWeightResult(); + if (!weight.isNull()) { + accumulator.accumulateWeight(weight); } } @@ -429,16 +428,16 @@ public boolean auditTemporaryNodes() { } private boolean auditTemporaryNodes(DataType dataType) { - MutateResult result = getData(dataType).removeIf(Node::hasExpired); + Difference result = getData(dataType).removeIf(Node::hasExpired); if (!result.isEmpty()) { invalidateCache(); + this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, result); } - this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, result); return !result.isEmpty(); } public Tristate hasNode(DataType type, Node node, NodeEqualityPredicate equalityPredicate) { - if (this.getType() == HolderType.GROUP && node instanceof InheritanceNode && ((InheritanceNode) node).getGroupName().equalsIgnoreCase(getObjectName())) { + if (this.getType() == HolderType.GROUP && node instanceof InheritanceNode && ((InheritanceNode) node).getGroupName().equalsIgnoreCase(getIdentifier().getName())) { return Tristate.TRUE; } @@ -463,7 +462,7 @@ public DataMutateResult setNode(DataType dataType, Node node, boolean callEvent) return DataMutateResult.FAIL_ALREADY_HAS; } - MutateResult changes = getData(dataType).add(node); + Difference changes = getData(dataType).add(node); invalidateCache(); if (callEvent) { this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, changes); @@ -500,7 +499,7 @@ public DataMutateResult.WithMergedNode setNode(DataType dataType, Node node, Tem if (newNode != null) { // Remove the old Node & add the new one. - MutateResult changes = data.removeThenAdd(otherMatch, newNode); + Difference changes = data.removeThenAdd(otherMatch, newNode); invalidateCache(); this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, changes); @@ -518,7 +517,7 @@ public DataMutateResult unsetNode(DataType dataType, Node node) { return DataMutateResult.FAIL_LACKS; } - MutateResult changes = getData(dataType).remove(node); + Difference changes = getData(dataType).remove(node); invalidateCache(); this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, changes); @@ -540,7 +539,7 @@ public DataMutateResult.WithMergedNode unsetNode(DataType dataType, Node node, @ Node newNode = node.toBuilder().expiry(newExpiry).build(); // Remove the old Node & add the new one. - MutateResult changes = data.removeThenAdd(otherMatch, newNode); + Difference changes = data.removeThenAdd(otherMatch, newNode); invalidateCache(); this.plugin.getEventDispatcher().dispatchNodeChanges(this, dataType, changes); @@ -554,7 +553,7 @@ public DataMutateResult.WithMergedNode unsetNode(DataType dataType, Node node, @ } public boolean removeIf(DataType dataType, @Nullable ContextSet contextSet, Predicate predicate, boolean giveDefault) { - MutateResult changes; + Difference changes; if (contextSet == null) { changes = getData(dataType).removeIf(predicate); } else { @@ -575,7 +574,7 @@ public boolean removeIf(DataType dataType, @Nullable ContextSet contextSet, Pred } public boolean clearNodes(DataType dataType, ContextSet contextSet, boolean giveDefault) { - MutateResult changes; + Difference changes; if (contextSet == null) { changes = getData(dataType).clear(); } else { @@ -591,8 +590,13 @@ public boolean clearNodes(DataType dataType, ContextSet contextSet, boolean give return true; } + public IntegerResult getWeightResult() { + return IntegerResult.nullResult(); + } + public OptionalInt getWeight() { - return OptionalInt.empty(); + IntegerResult result = getWeightResult(); + return result.isNull() ? OptionalInt.empty() : OptionalInt.of(result.intResult()); } private static final class MergedNodeResult implements DataMutateResult.WithMergedNode { diff --git a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolderIdentifier.java b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolderIdentifier.java index 36f7f8101..8faf2eded 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolderIdentifier.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/PermissionHolderIdentifier.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.model; import net.luckperms.api.model.PermissionHolder.Identifier; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; @@ -36,13 +35,15 @@ public final class PermissionHolderIdentifier implements Identifier { private final String name; public PermissionHolderIdentifier(HolderType type, String name) { - this.type = type == HolderType.USER ? Identifier.USER_TYPE : Identifier.GROUP_TYPE; - this.name = name; + this.type = Objects.requireNonNull(type, "type") == HolderType.USER + ? Identifier.USER_TYPE + : Identifier.GROUP_TYPE; + this.name = Objects.requireNonNull(name, "name"); } - @Override - public @NonNull String getName() { - return this.name; + public PermissionHolderIdentifier(String type, String name) { + this.type = Objects.requireNonNull(type, "type"); + this.name = Objects.requireNonNull(name, "name"); } @Override @@ -50,17 +51,26 @@ public PermissionHolderIdentifier(HolderType type, String name) { return this.type; } + @Override + public @NonNull String getName() { + return this.name; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Identifier)) return false; Identifier that = (Identifier) o; - return getType().equals(that.getType()) && - getName().equals(that.getName()); + return this.type.equals(that.getType()) && this.name.equals(that.getName()); } @Override public int hashCode() { - return Objects.hash(getType(), getName()); + return Objects.hash(this.type, this.name); + } + + @Override + public String toString() { + return this.type + '/' + this.name; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/PrimaryGroupHolder.java b/common/src/main/java/me/lucko/luckperms/common/model/PrimaryGroupHolder.java index 1a1dc7b1a..1e8368cd9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/PrimaryGroupHolder.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/PrimaryGroupHolder.java @@ -27,11 +27,11 @@ import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.inheritance.InheritanceGraph; - import net.luckperms.api.node.types.InheritanceNode; import net.luckperms.api.query.QueryOptions; import java.util.LinkedHashSet; +import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -90,7 +90,7 @@ public void setStoredValue(String value) { if (value == null || value.isEmpty()) { this.value = null; } else { - this.value = value.toLowerCase(); + this.value = value.toLowerCase(Locale.ROOT); } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/PromotionResults.java b/common/src/main/java/me/lucko/luckperms/common/model/PromotionResults.java index f19898d44..213e49d37 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/PromotionResults.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/PromotionResults.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.model; import net.luckperms.api.track.PromotionResult; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; diff --git a/common/src/main/java/me/lucko/luckperms/common/model/Track.java b/common/src/main/java/me/lucko/luckperms/common/model/Track.java index b06b5bd3c..0eff019b7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/Track.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/Track.java @@ -26,13 +26,11 @@ package me.lucko.luckperms.common.model; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.api.implementation.ApiTrack; import me.lucko.luckperms.common.model.manager.group.GroupManager; import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; - import net.luckperms.api.context.ContextSet; import net.luckperms.api.model.data.DataMutateResult; import net.luckperms.api.model.data.DataType; @@ -40,7 +38,6 @@ import net.luckperms.api.node.types.InheritanceNode; import net.luckperms.api.track.DemotionResult; import net.luckperms.api.track.PromotionResult; - import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; diff --git a/common/src/main/java/me/lucko/luckperms/common/model/User.java b/common/src/main/java/me/lucko/luckperms/common/model/User.java index 24ce90573..35a2bd440 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/User.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/User.java @@ -29,10 +29,8 @@ import me.lucko.luckperms.common.cacheddata.UserCachedDataManager; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.kyori.adventure.text.Component; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Optional; @@ -62,7 +60,7 @@ public class User extends PermissionHolder { private final UserCachedDataManager cachedData; public User(UUID uniqueId, LuckPermsPlugin plugin) { - super(plugin); + super(plugin, uniqueId.toString()); this.uniqueId = uniqueId; this.primaryGroup = plugin.getConfiguration().get(ConfigKeys.PRIMARY_GROUP_CALCULATION).apply(this); this.cachedData = new UserCachedDataManager(this); @@ -77,11 +75,6 @@ public Optional getUsername() { return Optional.ofNullable(this.username); } - @Override - public String getObjectName() { - return this.uniqueId.toString(); - } - @Override public Component getFormattedDisplayName() { return Component.text(getPlainDisplayName()); diff --git a/common/src/main/java/me/lucko/luckperms/common/model/WeightCache.java b/common/src/main/java/me/lucko/luckperms/common/model/WeightCache.java index c3231afef..6b947ed7d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/WeightCache.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/WeightCache.java @@ -26,21 +26,20 @@ package me.lucko.luckperms.common.model; import me.lucko.luckperms.common.cache.Cache; +import me.lucko.luckperms.common.cacheddata.result.IntegerResult; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.query.QueryOptionsImpl; - import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.WeightNode; - import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Locale; import java.util.Map; -import java.util.OptionalInt; /** * Cache instance to supply the weight of a {@link Group}. */ -public class WeightCache extends Cache { +public class WeightCache extends Cache> { private final Group group; public WeightCache(Group group) { @@ -48,27 +47,24 @@ public WeightCache(Group group) { } @Override - protected @NonNull OptionalInt supply() { - boolean seen = false; - int weight = 0; + protected @NonNull IntegerResult supply() { + IntegerResult weight = null; for (WeightNode n : this.group.getOwnNodes(NodeType.WEIGHT, QueryOptionsImpl.DEFAULT_NON_CONTEXTUAL)) { int value = n.getWeight(); - if (!seen || value > weight) { - seen = true; - weight = value; + if (weight == null || value > weight.intResult()) { + weight = IntegerResult.of(n); } } - if (!seen) { + if (weight == null) { Map configWeights = this.group.getPlugin().getConfiguration().get(ConfigKeys.GROUP_WEIGHTS); - Integer value = configWeights.get(this.group.getObjectName().toLowerCase()); + Integer value = configWeights.get(this.group.getIdentifier().getName().toLowerCase(Locale.ROOT)); if (value != null) { - seen = true; - weight = value; + weight = IntegerResult.of(value); } } - return seen ? OptionalInt.of(weight) : OptionalInt.empty(); + return weight != null ? weight : IntegerResult.nullResult(); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/AbstractManager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/AbstractManager.java index 1df7651d6..871968e6f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/AbstractManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/AbstractManager.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.model.manager; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.cache.LoadingMap; import java.util.Collection; diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/group/AbstractGroupManager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/group/AbstractGroupManager.java index b600e4cf6..623599f3d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/group/AbstractGroupManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/group/AbstractGroupManager.java @@ -28,6 +28,7 @@ import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.manager.AbstractManager; +import java.util.Locale; import java.util.Optional; public abstract class AbstractGroupManager extends AbstractManager implements GroupManager { @@ -61,7 +62,7 @@ public T getByDisplayName(String name) { @Override protected String sanitizeIdentifier(String s) { - return s.toLowerCase(); + return s.toLowerCase(Locale.ROOT); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/track/AbstractTrackManager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/track/AbstractTrackManager.java index f6cff287f..22469e90e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/track/AbstractTrackManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/track/AbstractTrackManager.java @@ -28,10 +28,12 @@ import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.manager.AbstractManager; +import java.util.Locale; + public abstract class AbstractTrackManager extends AbstractManager implements TrackManager { @Override protected String sanitizeIdentifier(String s) { - return s.toLowerCase(); + return s.toLowerCase(Locale.ROOT); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/AbstractUserManager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/AbstractUserManager.java index c25c76798..3b84e3399 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/AbstractUserManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/AbstractUserManager.java @@ -26,14 +26,14 @@ package me.lucko.luckperms.common.model.manager.user; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.AbstractManager; import me.lucko.luckperms.common.model.manager.group.GroupManager; import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; - +import me.lucko.luckperms.common.util.CompletableFutures; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; import net.luckperms.api.node.types.InheritanceNode; @@ -86,7 +86,7 @@ public boolean giveDefaultIfNeeded(User user) { // check that they are actually a member of their primary group, otherwise remove it if (this.plugin.getConfiguration().get(ConfigKeys.PRIMARY_GROUP_CALCULATION_METHOD).equals("stored")) { - String primaryGroup = user.getCachedData().getMetaData(this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS)).getPrimaryGroup(MetaCheckEvent.Origin.INTERNAL); + String primaryGroup = user.getCachedData().getMetaData(this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS)).getPrimaryGroup(CheckOrigin.INTERNAL); boolean memberOfPrimaryGroup = false; for (InheritanceNode node : globalGroups) { @@ -165,11 +165,9 @@ public CompletableFuture loadAllUsers() { Set ids = new HashSet<>(getAll().keySet()); ids.addAll(this.plugin.getBootstrap().getOnlinePlayers()); - CompletableFuture[] loadTasks = ids.stream() + return ids.stream() .map(id -> this.plugin.getStorage().loadUser(id, null)) - .toArray(CompletableFuture[]::new); - - return CompletableFuture.allOf(loadTasks); + .collect(CompletableFutures.collector()); } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/StandardUserManager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/StandardUserManager.java index 1b0f62550..61c8a66a0 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/StandardUserManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/StandardUserManager.java @@ -35,7 +35,7 @@ public class StandardUserManager extends AbstractUserManager { private final LuckPermsPlugin plugin; public StandardUserManager(LuckPermsPlugin plugin) { - super(plugin, UserHousekeeper.timeoutSettings(1, TimeUnit.MINUTES)); + super(plugin, UserHousekeeper.timeoutSettings(2, TimeUnit.MINUTES)); this.plugin = plugin; } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserHousekeeper.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserHousekeeper.java index f1ae13665..c458d2440 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserHousekeeper.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserHousekeeper.java @@ -30,6 +30,7 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.util.ExpiringSet; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -41,16 +42,16 @@ public class UserHousekeeper implements Runnable { private final UserManager userManager; // contains the uuids of users who have recently logged in / out - private final ExpiringSet recentlyUsed; + private final Set recentlyUsed; // contains the uuids of users who have recently been retrieved from the API - private final ExpiringSet recentlyUsedApi; + private final Set recentlyUsedApi; public UserHousekeeper(LuckPermsPlugin plugin, UserManager userManager, TimeoutSettings timeoutSettings) { this.plugin = plugin; this.userManager = userManager; - this.recentlyUsed = new ExpiringSet<>(timeoutSettings.duration, timeoutSettings.unit); - this.recentlyUsedApi = new ExpiringSet<>(5, TimeUnit.MINUTES); + this.recentlyUsed = ExpiringSet.newExpiringSet(timeoutSettings.duration, timeoutSettings.unit); + this.recentlyUsedApi = ExpiringSet.newExpiringSet(5, TimeUnit.MINUTES); } // called when a player attempts a connection or logs out diff --git a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserManager.java b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserManager.java index 7b64a2561..175caf382 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/manager/user/UserManager.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.calculator.PermissionCalculator; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.Manager; - import net.luckperms.api.node.Node; import java.util.UUID; diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/MutateResult.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/MutateResult.java deleted file mode 100644 index aeb554c1f..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/MutateResult.java +++ /dev/null @@ -1,152 +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.common.model.nodemap; - -import net.luckperms.api.node.Node; - -import java.util.LinkedHashSet; -import java.util.Objects; -import java.util.Set; - -/** - * Records a log of the changes that occur as a result of a {@link NodeMap} mutation(s). - */ -public class MutateResult { - private final LinkedHashSet changes = new LinkedHashSet<>(); - - public Set getChanges() { - return this.changes; - } - - public Set getChanges(ChangeType type) { - Set changes = new LinkedHashSet<>(this.changes.size()); - for (Change change : this.changes) { - if (change.getType() == type) { - changes.add(change.getNode()); - } - } - return changes; - } - - void clear() { - this.changes.clear(); - } - - public boolean isEmpty() { - return this.changes.isEmpty(); - } - - public Set getAdded() { - return getChanges(ChangeType.ADD); - } - - public Set getRemoved() { - return getChanges(ChangeType.REMOVE); - } - - private void recordChange(Change change) { - // This method is the magic of this class. - // When tracking, we want to ignore changes that cancel each other out, and only - // keep track of the net difference. - // e.g. adding then removing the same node = zero net change, so ignore it. - - if (this.changes.remove(change.inverse())) { - return; - } - this.changes.add(change); - } - - public void recordChange(ChangeType type, Node node) { - recordChange(new Change(type, node)); - } - - public void recordChanges(ChangeType type, Iterable nodes) { - for (Node node : nodes) { - recordChange(new Change(type, node)); - } - } - - public MutateResult mergeFrom(MutateResult other) { - for (Change change : other.changes) { - recordChange(change); - } - return this; - } - - @Override - public String toString() { - return "MutateResult{changes=" + this.changes + '}'; - } - - public static final class Change { - private final ChangeType type; - private final Node node; - - public Change(ChangeType type, Node node) { - this.type = type; - this.node = node; - } - - public ChangeType getType() { - return this.type; - } - - public Node getNode() { - return this.node; - } - - public Change inverse() { - return new Change(this.type.inverse(), this.node); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Change change = (Change) o; - return this.type == change.type && this.node.equals(change.node); - } - - @Override - public int hashCode() { - return Objects.hash(this.type, this.node); - } - - @Override - public String toString() { - return "Change{type=" + this.type + ", node=" + this.node + '}'; - } - } - - public enum ChangeType { - ADD, REMOVE; - - public ChangeType inverse() { - return this == ADD ? REMOVE : ADD; - } - } - -} diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMap.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMap.java index 117045cac..f4c919828 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMap.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMap.java @@ -27,10 +27,9 @@ import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableSet; - import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.node.comparator.NodeWithContextComparator; - +import me.lucko.luckperms.common.util.Difference; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.Node; @@ -71,7 +70,7 @@ default LinkedHashSet asSet() { } default SortedSet asSortedSet() { - SortedSet set = new TreeSet<>(NodeWithContextComparator.reverse()); + SortedSet set = new TreeSet<>(NodeWithContextComparator.descending()); copyTo(set); return set; } @@ -97,7 +96,7 @@ default LinkedHashSet inheritanceAsSet() { } default SortedSet inheritanceAsSortedSet() { - SortedSet set = new TreeSet<>(NodeWithContextComparator.reverse()); + SortedSet set = new TreeSet<>(NodeWithContextComparator.descending()); copyInheritanceNodesTo(set); return set; } @@ -134,28 +133,28 @@ default ImmutableSet inheritanceAsImmutableSet() { // mutate methods - MutateResult add(Node nodeWithoutInheritanceOrigin); + Difference add(Node nodeWithoutInheritanceOrigin); - MutateResult remove(Node node); + Difference remove(Node node); - MutateResult removeExact(Node node); + Difference removeExact(Node node); - MutateResult removeIf(Predicate predicate); + Difference removeIf(Predicate predicate); - MutateResult removeIf(ContextSet contextSet, Predicate predicate); + Difference removeIf(ContextSet contextSet, Predicate predicate); - MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd); + Difference removeThenAdd(Node nodeToRemove, Node nodeToAdd); - MutateResult clear(); + Difference clear(); - MutateResult clear(ContextSet contextSet); + Difference clear(ContextSet contextSet); - MutateResult setContent(Iterable set); + Difference setContent(Iterable set); - MutateResult setContent(Stream stream); + Difference applyChanges(Difference changes); - MutateResult addAll(Iterable set); + Difference addAll(Iterable set); - MutateResult addAll(Stream stream); + Difference addAll(Stream stream); } diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapBase.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapBase.java index 3274e8a01..ebb33a6ce 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapBase.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapBase.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.model.nodemap; import com.google.common.collect.ImmutableCollection; - import net.luckperms.api.context.ContextSatisfyMode; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; diff --git a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapMutable.java b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapMutable.java index 9b500c15f..edec2371a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapMutable.java +++ b/common/src/main/java/me/lucko/luckperms/common/model/nodemap/NodeMapMutable.java @@ -26,15 +26,16 @@ package me.lucko.luckperms.common.model.nodemap; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.ContextSetComparator; +import me.lucko.luckperms.common.context.comparator.ContextSetComparator; import me.lucko.luckperms.common.model.InheritanceOrigin; import me.lucko.luckperms.common.model.PermissionHolder; -import me.lucko.luckperms.common.model.nodemap.MutateResult.ChangeType; import me.lucko.luckperms.common.node.comparator.NodeComparator; - +import me.lucko.luckperms.common.util.Difference; +import me.lucko.luckperms.common.util.Difference.ChangeType; import net.luckperms.api.context.ContextSatisfyMode; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeEqualityPredicate; import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; @@ -56,12 +57,12 @@ public class NodeMapMutable extends NodeMapBase { // Used in calls to Map#computeIfAbsent to make them behave like a LoadingMap/Cache // The key (ImmutableContextSet) isn't actually used - these are more like suppliers than functions - private static final Function> VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse()); - private static final Function> INHERITANCE_VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.reverse()); + private static final Function> VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.descending()); + private static final Function> INHERITANCE_VALUE_SET_SUPPLIER = k -> new ConcurrentSkipListSet<>(NodeComparator.descending()); // Creates the Map instances used by this.map and this.inheritanceMap private static SortedMap> createMap() { - return new ConcurrentSkipListMap<>(ContextSetComparator.reverse()); + return new ConcurrentSkipListMap<>(ContextSetComparator.descending()); } /* @@ -70,28 +71,28 @@ private static SortedMap> cre * The node values are ordered according to the priority rules defined in NodeComparator. * * We use our own "multimap"-like implementation here because guava's is not thread safe. - * - * The map fields aren't final because they are replaced when large updates (e.g. clear) - * are performed. We do this so there's no risk that the read methods will see an inconsistent - * state in the middle of an update from the DB. (see below comment about locking - we don't - * lock for reads!) */ - private SortedMap> map = createMap(); - private SortedMap> inheritanceMap = createMap(); + private final SortedMap> map = createMap(); + private final SortedMap> inheritanceMap = createMap(); /** - * This lock is used whilst performing mutations, but *not* reads. + * This lock is used whilst performing writes, but *not* reads. * * The maps themselves are thread safe, so for querying, we just allow * the read methods to do whatever they want without any locking. - * However, we want mutations to be atomic, so we use the lock to ensure that happens. + * + * Readers can see partially inconsistent data, but this is fine - the data is only ever used for + * querying, and the worst case scenario is that a query returns a node that has just been removed, + * or doesn't return a node that has just been added. */ private final Lock lock = new ReentrantLock(); protected final PermissionHolder holder; + private final InheritanceOrigin inheritanceOrigin; - public NodeMapMutable(PermissionHolder holder) { + public NodeMapMutable(PermissionHolder holder, DataType type) { this.holder = holder; + this.inheritanceOrigin = new InheritanceOrigin(holder.getIdentifier(), type); } @Override @@ -110,20 +111,20 @@ protected ContextSatisfyMode defaultSatisfyMode() { } private Node addInheritanceOrigin(Node node) { - Optional metadata = node.getMetadata(InheritanceOriginMetadata.KEY); - if (metadata.isPresent() && metadata.get().getOrigin().equals(this.holder.getIdentifier())) { + Optional existing = node.getMetadata(InheritanceOriginMetadata.KEY); + if (existing.isPresent() && existing.get().equals(this.inheritanceOrigin)) { return node; } - return node.toBuilder().withMetadata(InheritanceOriginMetadata.KEY, new InheritanceOrigin(this.holder.getIdentifier())).build(); + return node.toBuilder().withMetadata(InheritanceOriginMetadata.KEY, this.inheritanceOrigin).build(); } @Override - public MutateResult add(Node nodeWithoutInheritanceOrigin) { + public Difference add(Node nodeWithoutInheritanceOrigin) { Node node = addInheritanceOrigin(nodeWithoutInheritanceOrigin); ImmutableContextSet context = node.getContexts(); - MutateResult result = new MutateResult(); + Difference result = new Difference<>(); this.lock.lock(); try { @@ -159,9 +160,9 @@ public MutateResult add(Node nodeWithoutInheritanceOrigin) { } @Override - public MutateResult remove(Node node) { + public Difference remove(Node node) { ImmutableContextSet context = node.getContexts(); - MutateResult result = new MutateResult(); + Difference result = new Difference<>(); this.lock.lock(); try { @@ -188,7 +189,7 @@ public MutateResult remove(Node node) { return result; } - private static void removeMatching(Iterator it, Node node, MutateResult result) { + private static void removeMatching(Iterator it, Node node, Difference result) { while (it.hasNext()) { Node el = it.next(); if (node.equals(el, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)) { @@ -198,7 +199,7 @@ private static void removeMatching(Iterator it, Node node, MutateResult re } } - private static void removeMatchingButNotSame(Iterator it, Node node, MutateResult result) { + private static void removeMatchingButNotSame(Iterator it, Node node, Difference result) { while (it.hasNext()) { Node el = it.next(); if (el != node && node.equals(el, NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE)) { @@ -209,9 +210,9 @@ private static void removeMatchingButNotSame(Iterator it, Node node, Mutat } @Override - public MutateResult removeExact(Node node) { + public Difference removeExact(Node node) { ImmutableContextSet context = node.getContexts(); - MutateResult result = new MutateResult(); + Difference result = new Difference<>(); this.lock.lock(); try { @@ -242,8 +243,8 @@ public MutateResult removeExact(Node node) { } @Override - public MutateResult removeIf(Predicate predicate) { - MutateResult result = new MutateResult(); + public Difference removeIf(Predicate predicate) { + Difference result = new Difference<>(); this.lock.lock(); try { @@ -258,9 +259,9 @@ public MutateResult removeIf(Predicate predicate) { } @Override - public MutateResult removeIf(ContextSet contextSet, Predicate predicate) { + public Difference removeIf(ContextSet contextSet, Predicate predicate) { ImmutableContextSet context = contextSet.immutableCopy(); - MutateResult result = new MutateResult(); + Difference result = new Difference<>(); this.lock.lock(); try { @@ -276,7 +277,7 @@ public MutateResult removeIf(ContextSet contextSet, Predicate pred return result; } - private void removeMatching(Iterator it, Predicate predicate, MutateResult result) { + private void removeMatching(Iterator it, Predicate predicate, Difference result) { while (it.hasNext()) { Node node = it.next(); @@ -297,9 +298,9 @@ private void removeMatching(Iterator it, Predicate predicate } @Override - public MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd) { + public Difference removeThenAdd(Node nodeToRemove, Node nodeToAdd) { if (nodeToAdd.equals(nodeToRemove)) { - return new MutateResult(); + return new Difference<>(); } this.lock.lock(); @@ -311,8 +312,8 @@ public MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd) { } @Override - public MutateResult clear() { - MutateResult result = new MutateResult(); + public Difference clear() { + Difference result = new Difference<>(); this.lock.lock(); try { @@ -321,10 +322,8 @@ public MutateResult clear() { result.recordChanges(ChangeType.REMOVE, nodes); } - // replace the map - this means any client reading async won't be affected - // by any race conditions between this call to clear and any subsequent call to setContent - this.map = createMap(); - this.inheritanceMap = createMap(); + this.map.clear(); + this.inheritanceMap.clear(); } finally { this.lock.unlock(); } @@ -333,9 +332,9 @@ public MutateResult clear() { } @Override - public MutateResult clear(ContextSet contextSet) { + public Difference clear(ContextSet contextSet) { ImmutableContextSet context = contextSet.immutableCopy(); - MutateResult result = new MutateResult(); + Difference result = new Difference<>(); this.lock.lock(); try { @@ -352,28 +351,33 @@ public MutateResult clear(ContextSet contextSet) { } @Override - public MutateResult setContent(Iterable set) { - MutateResult result = new MutateResult(); + public Difference setContent(Iterable set) { + Difference diff = new Difference<>(); + diff.recordChanges(ChangeType.ADD, set); this.lock.lock(); try { - result.mergeFrom(clear()); - result.mergeFrom(addAll(set)); + for (SortedSet nodes : this.map.values()) { + diff.recordChanges(ChangeType.REMOVE, nodes); + } + return applyChanges(diff); } finally { this.lock.unlock(); } - - return result; } @Override - public MutateResult setContent(Stream stream) { - MutateResult result = new MutateResult(); + public Difference applyChanges(Difference changes) { + Difference result = new Difference<>(); this.lock.lock(); try { - result.mergeFrom(clear()); - result.mergeFrom(addAll(stream)); + for (Node n : changes.getRemoved()) { + result.mergeFrom(removeExact(n)); + } + for (Node n : changes.getAdded()) { + result.mergeFrom(add(n)); + } } finally { this.lock.unlock(); } @@ -382,8 +386,8 @@ public MutateResult setContent(Stream stream) { } @Override - public MutateResult addAll(Iterable set) { - MutateResult result = new MutateResult(); + public Difference addAll(Iterable set) { + Difference result = new Difference<>(); this.lock.lock(); try { @@ -398,8 +402,8 @@ public MutateResult addAll(Iterable set) { } @Override - public MutateResult addAll(Stream stream) { - MutateResult result = new MutateResult(); + public Difference addAll(Stream stream) { + Difference result = new Difference<>(); this.lock.lock(); try { 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 10d24cad9..4e39c6fb0 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,7 +27,9 @@ 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; import net.luckperms.api.node.Node; @@ -43,6 +45,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -53,7 +56,7 @@ public class RecordedNodeMap implements NodeMap { private final NodeMap delegate; private final Lock lock = new ReentrantLock(); - private MutateResult changes = new MutateResult(); + private Difference changes = new Difference<>(); public RecordedNodeMap(NodeMap delegate) { this.delegate = delegate; @@ -72,12 +75,12 @@ public void discardChanges() { } } - public MutateResult exportChanges(Predicate onlyIf) { + public Difference exportChanges(Predicate> onlyIf) { this.lock.lock(); try { - MutateResult existing = this.changes; + Difference existing = this.changes; if (onlyIf.test(existing)) { - this.changes = new MutateResult(); + this.changes = new Difference<>(); return existing; } return null; @@ -86,76 +89,83 @@ public MutateResult exportChanges(Predicate onlyIf) { } } - private MutateResult record(MutateResult result) { + public Difference addDefaultNodeToChangeSet() { + Difference diff = new Difference<>(); + diff.recordChange(Difference.ChangeType.ADD, Inheritance.builder(GroupManager.DEFAULT_GROUP_NAME).build()); + return record(map -> diff); + } + + private Difference record(Function> func) { this.lock.lock(); try { + Difference result = func.apply(this.delegate); this.changes.mergeFrom(result); + return result; } finally { this.lock.unlock(); } - return result; } // delegate, but pass the result through #record(MutateResult) @Override - public MutateResult add(Node nodeWithoutInheritanceOrigin) { - return record(this.delegate.add(nodeWithoutInheritanceOrigin)); + public Difference add(Node nodeWithoutInheritanceOrigin) { + return record(map -> map.add(nodeWithoutInheritanceOrigin)); } @Override - public MutateResult remove(Node node) { - return record(this.delegate.remove(node)); + public Difference remove(Node node) { + return record(map -> map.remove(node)); } @Override - public MutateResult removeExact(Node node) { - return record(this.delegate.removeExact(node)); + public Difference removeExact(Node node) { + return record(map -> map.removeExact(node)); } @Override - public MutateResult removeIf(Predicate predicate) { - return record(this.delegate.removeIf(predicate)); + public Difference removeIf(Predicate predicate) { + return record(map -> map.removeIf(predicate)); } @Override - public MutateResult removeIf(ContextSet contextSet, Predicate predicate) { - return record(this.delegate.removeIf(contextSet, predicate)); + public Difference removeIf(ContextSet contextSet, Predicate predicate) { + return record(map -> map.removeIf(contextSet, predicate)); } @Override - public MutateResult removeThenAdd(Node nodeToRemove, Node nodeToAdd) { - return record(this.delegate.removeThenAdd(nodeToRemove, nodeToAdd)); + public Difference removeThenAdd(Node nodeToRemove, Node nodeToAdd) { + return record(map -> map.removeThenAdd(nodeToRemove, nodeToAdd)); } @Override - public MutateResult clear() { - return record(this.delegate.clear()); + public Difference clear() { + return record(map -> map.clear()); } @Override - public MutateResult clear(ContextSet contextSet) { - return record(this.delegate.clear(contextSet)); + public Difference clear(ContextSet contextSet) { + return record(map -> map.clear(contextSet)); } @Override - public MutateResult setContent(Iterable set) { - return record(this.delegate.setContent(set)); + public Difference setContent(Iterable set) { + return record(map -> map.setContent(set)); } @Override - public MutateResult setContent(Stream stream) { - return record(this.delegate.setContent(stream)); + public Difference applyChanges(Difference changes) { + return record(map -> map.applyChanges(changes)); } @Override - public MutateResult addAll(Iterable set) { - return record(this.delegate.addAll(set)); + public Difference addAll(Iterable set) { + return record(map -> map.addAll(set)); } @Override - public MutateResult addAll(Stream stream) { - return record(this.delegate.addAll(stream)); + public Difference addAll(Stream stream) { + return record(map -> map.addAll(stream)); } // just plain delegation diff --git a/common/src/main/java/me/lucko/luckperms/common/node/AbstractNode.java b/common/src/main/java/me/lucko/luckperms/common/node/AbstractNode.java index aaa0c11cb..2c9c96b2e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/AbstractNode.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/AbstractNode.java @@ -27,9 +27,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.node.utils.ShorthandParser; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeBuilder; @@ -37,7 +35,6 @@ import net.luckperms.api.node.ScopedNode; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.node.types.PermissionNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -76,7 +73,9 @@ protected AbstractNode(String key, boolean value, long expireAt, ImmutableContex this.contexts = contexts; this.metadata = ImmutableMap.copyOf(metadata); - this.resolvedShorthand = this instanceof PermissionNode ? ImmutableList.copyOf(ShorthandParser.expandShorthand(this.key)) : ImmutableList.of(); + this.resolvedShorthand = this instanceof PermissionNode + ? ImmutableList.copyOf(ShorthandParser.expandShorthandSafely(this.key)) + : ImmutableList.of(); this.hashCode = calculateHashCode(); } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/AbstractNodeBuilder.java b/common/src/main/java/me/lucko/luckperms/common/node/AbstractNodeBuilder.java index 00fde5c4d..ca2a97b79 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/AbstractNodeBuilder.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/AbstractNodeBuilder.java @@ -25,14 +25,12 @@ package me.lucko.luckperms.common.node; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; - +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.ScopedNode; import net.luckperms.api.node.metadata.NodeMetadataKey; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeComparator.java b/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeComparator.java index dc2e639a5..d672b8476 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeComparator.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeComparator.java @@ -26,20 +26,21 @@ package me.lucko.luckperms.common.node.comparator; import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.PermissionNode; import java.util.Comparator; public class NodeComparator implements Comparator { - private static final Comparator INSTANCE = new NodeComparator(); - private static final Comparator REVERSE = INSTANCE.reversed(); + private static final Comparator ASCENDING = new NodeComparator(); + private static final Comparator DESCENDING = ASCENDING.reversed(); - public static Comparator normal() { - return INSTANCE; + public static Comparator ascending() { + return ASCENDING; } - public static Comparator reverse() { - return REVERSE; + public static Comparator descending() { + return DESCENDING; } @SuppressWarnings({"ConstantConditions", "OptionalGetWithoutIsPresent"}) @@ -49,13 +50,20 @@ public int compare(Node o1, Node o2) { return 0; } - // compare whether nodes are temporary - int result = Boolean.compare(o1.hasExpiry(), o2.hasExpiry()); + // compare node types - special types (inheritance, prefix, etc) have priority over standard permissions + //noinspection unchecked + int result = -((Comparable>) o1.getType()).compareTo(o2.getType()); if (result != 0) { return result; } - // compare whether nodes are wildcard nodes + // compare whether nodes are temporary - temporary has priority + result = Boolean.compare(o1.hasExpiry(), o2.hasExpiry()); + if (result != 0) { + return result; + } + + // compare whether nodes are wildcard nodes - non-wildcard has priority result = Boolean.compare( o1 instanceof PermissionNode && ((PermissionNode) o1).isWildcard(), o2 instanceof PermissionNode && ((PermissionNode) o2).isWildcard() @@ -66,8 +74,9 @@ public int compare(Node o1, Node o2) { // compare expiry times if both nodes are temporary // due to the comparison earlier, either both nodes are temporary or neither are. + // sooner expiry has priority if (o1.hasExpiry()) { - result = o1.getExpiry().compareTo(o2.getExpiry()); + result = -o1.getExpiry().compareTo(o2.getExpiry()); if (result != 0) { return result; } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeEntryComparator.java b/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeEntryComparator.java index 1165d098d..19cfc0ab4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeEntryComparator.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeEntryComparator.java @@ -31,17 +31,17 @@ public class NodeEntryComparator> implements Comparator> { - public static > Comparator> normal() { + public static > Comparator> ascending() { return new NodeEntryComparator<>(); } - public static > Comparator> reverse() { - return NodeEntryComparator.normal().reversed(); + public static > Comparator> descending() { + return NodeEntryComparator.ascending().reversed(); } @Override public int compare(NodeEntry o1, NodeEntry o2) { - int i = NodeWithContextComparator.normal().compare(o1.getNode(), o2.getNode()); + int i = NodeWithContextComparator.ascending().compare(o1.getNode(), o2.getNode()); if (i != 0) { return i; } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeWithContextComparator.java b/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeWithContextComparator.java index 2b84a53c5..2b59fc48e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeWithContextComparator.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/comparator/NodeWithContextComparator.java @@ -25,26 +25,25 @@ package me.lucko.luckperms.common.node.comparator; -import me.lucko.luckperms.common.context.ContextSetComparator; - +import me.lucko.luckperms.common.context.comparator.ContextSetComparator; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.Node; import java.util.Comparator; /** - * Compares permission nodes based upon their supposed "priority". + * Compares permission nodes based upon their "priority". */ public class NodeWithContextComparator implements Comparator { - private static final Comparator INSTANCE = new NodeWithContextComparator(ContextSetComparator.normal(), NodeComparator.normal()); - private static final Comparator REVERSE = new NodeWithContextComparator(ContextSetComparator.reverse(), NodeComparator.reverse()); + private static final Comparator ASCENDING = new NodeWithContextComparator(ContextSetComparator.ascending(), NodeComparator.ascending()); + private static final Comparator DESCENDING = new NodeWithContextComparator(ContextSetComparator.descending(), NodeComparator.descending()); - public static Comparator normal() { - return INSTANCE; + public static Comparator ascending() { + return ASCENDING; } - public static Comparator reverse() { - return REVERSE; + public static Comparator descending() { + return DESCENDING; } private final Comparator contextSetComparator; diff --git a/common/src/main/java/me/lucko/luckperms/common/node/factory/Delimiters.java b/common/src/main/java/me/lucko/luckperms/common/node/factory/Delimiters.java index e872e27fa..74886c2cf 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/factory/Delimiters.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/factory/Delimiters.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.node.factory; import com.google.common.base.Splitter; - import me.lucko.luckperms.common.cache.PatternCache; import me.lucko.luckperms.common.node.AbstractNode; diff --git a/common/src/main/java/me/lucko/luckperms/common/node/factory/NodeBuilders.java b/common/src/main/java/me/lucko/luckperms/common/node/factory/NodeBuilders.java index 6147058f6..00fad6501 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/factory/NodeBuilders.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/factory/NodeBuilders.java @@ -33,7 +33,6 @@ import me.lucko.luckperms.common.node.types.RegexPermission; import me.lucko.luckperms.common.node.types.Suffix; import me.lucko.luckperms.common.node.types.Weight; - import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.types.DisplayNameNode; import net.luckperms.api.node.types.InheritanceNode; @@ -42,7 +41,6 @@ import net.luckperms.api.node.types.RegexPermissionNode; import net.luckperms.api.node.types.SuffixNode; import net.luckperms.api.node.types.WeightNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/node/factory/NodeCommandFactory.java b/common/src/main/java/me/lucko/luckperms/common/node/factory/NodeCommandFactory.java index 47212c29a..c487a178d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/factory/NodeCommandFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/factory/NodeCommandFactory.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.node.factory; import me.lucko.luckperms.common.model.HolderType; - import net.luckperms.api.context.Context; import net.luckperms.api.node.Node; import net.luckperms.api.node.types.ChatMetaNode; diff --git a/common/src/main/java/me/lucko/luckperms/common/node/matcher/ConstraintNodeMatcher.java b/common/src/main/java/me/lucko/luckperms/common/node/matcher/ConstraintNodeMatcher.java index da6786246..d07f780e6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/matcher/ConstraintNodeMatcher.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/matcher/ConstraintNodeMatcher.java @@ -25,11 +25,11 @@ package me.lucko.luckperms.common.node.matcher; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; - +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.Constraint; +import me.lucko.luckperms.common.filter.ConstraintFactory; import net.luckperms.api.node.Node; import net.luckperms.api.node.matcher.NodeMatcher; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -37,20 +37,20 @@ * Abstract implementation of {@link NodeMatcher} backed by a {@link Constraint}. */ public abstract class ConstraintNodeMatcher implements NodeMatcher { - private final Constraint constraint; + private final Constraint constraint; - protected ConstraintNodeMatcher(Constraint constraint) { - this.constraint = constraint; + protected ConstraintNodeMatcher(Comparison comparison, String value) { + this.constraint = ConstraintFactory.STRINGS.build(comparison, value); } - public Constraint getConstraint() { + public Constraint getConstraint() { return this.constraint; } public abstract @Nullable T filterConstraintMatch(@NonNull Node node); public @Nullable T match(Node node) { - return getConstraint().eval(node.getKey()) ? filterConstraintMatch(node) : null; + return getConstraint().evaluate(node.getKey()) ? filterConstraintMatch(node) : null; } @Override diff --git a/common/src/main/java/me/lucko/luckperms/common/node/matcher/StandardNodeMatchers.java b/common/src/main/java/me/lucko/luckperms/common/node/matcher/StandardNodeMatchers.java index 9960268f9..c1b12785e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/matcher/StandardNodeMatchers.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/matcher/StandardNodeMatchers.java @@ -25,8 +25,7 @@ package me.lucko.luckperms.common.node.matcher; -import me.lucko.luckperms.common.bulkupdate.comparison.Constraint; -import me.lucko.luckperms.common.bulkupdate.comparison.StandardComparison; +import me.lucko.luckperms.common.filter.Comparison; import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.types.DisplayName; import me.lucko.luckperms.common.node.types.Inheritance; @@ -35,24 +34,22 @@ import me.lucko.luckperms.common.node.types.RegexPermission; import me.lucko.luckperms.common.node.types.Suffix; import me.lucko.luckperms.common.node.types.Weight; - import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeEqualityPredicate; import net.luckperms.api.node.NodeType; import net.luckperms.api.node.types.MetaNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; public final class StandardNodeMatchers { private StandardNodeMatchers() {} - public static ConstraintNodeMatcher of(Constraint constraint) { - return new Generic(constraint); + public static ConstraintNodeMatcher key(String value, Comparison comparison) { + return new Generic(comparison, value); } public static ConstraintNodeMatcher key(String key) { - return new Generic(Constraint.of(StandardComparison.EQUAL, key)); + return new Generic(Comparison.EQUAL, key); } public static ConstraintNodeMatcher key(T node) { @@ -60,7 +57,7 @@ public static ConstraintNodeMatcher key(T node) { } public static ConstraintNodeMatcher keyStartsWith(String startsWith) { - return new Generic(Constraint.of(StandardComparison.SIMILAR, startsWith + StandardComparison.WILDCARD)); + return new Generic(Comparison.SIMILAR, startsWith + Comparison.WILDCARD); } public static ConstraintNodeMatcher equals(T other, NodeEqualityPredicate equalityPredicate) { @@ -75,9 +72,9 @@ public static ConstraintNodeMatcher type(NodeType(type); } - private static class Generic extends ConstraintNodeMatcher { - Generic(Constraint constraint) { - super(constraint); + public static class Generic extends ConstraintNodeMatcher { + Generic(Comparison comparison, String value) { + super(comparison, value); } @Override @@ -86,16 +83,24 @@ private static class Generic extends ConstraintNodeMatcher { } } - private static final class NodeEquals extends ConstraintNodeMatcher { + public static final class NodeEquals extends ConstraintNodeMatcher { private final T node; private final NodeEqualityPredicate equalityPredicate; NodeEquals(T node, NodeEqualityPredicate equalityPredicate) { - super(Constraint.of(StandardComparison.EQUAL, node.getKey())); + super(Comparison.EQUAL, node.getKey()); this.node = node; this.equalityPredicate = equalityPredicate; } + public T getNode() { + return this.node; + } + + public NodeEqualityPredicate getEqualityPredicate() { + return this.equalityPredicate; + } + @SuppressWarnings("unchecked") @Override public @Nullable T filterConstraintMatch(@NonNull Node node) { @@ -106,9 +111,16 @@ private static final class NodeEquals extends ConstraintNodeMatc } } - private static final class MetaKeyEquals extends ConstraintNodeMatcher { + public static final class MetaKeyEquals extends ConstraintNodeMatcher { + private final String metaKey; + MetaKeyEquals(String metaKey) { - super(Constraint.of(StandardComparison.SIMILAR, Meta.key(metaKey, StandardComparison.WILDCARD))); + super(Comparison.SIMILAR, Meta.key(metaKey, Comparison.WILDCARD)); + this.metaKey = metaKey; + } + + public String getMetaKey() { + return this.metaKey; } @Override @@ -117,34 +129,38 @@ private static final class MetaKeyEquals extends ConstraintNodeMatcher } } - private static final class TypeEquals extends ConstraintNodeMatcher { + public static final class TypeEquals extends ConstraintNodeMatcher { private final NodeType type; - protected TypeEquals(NodeType type) { - super(getConstraintForType(type)); + TypeEquals(NodeType type) { + super(Comparison.SIMILAR, getSimilarToComparisonValue(type)); this.type = type; } + public NodeType getType() { + return this.type; + } + @Override public @Nullable T filterConstraintMatch(@NonNull Node node) { return this.type.tryCast(node).orElse(null); } - private static Constraint getConstraintForType(NodeType type) { + private static String getSimilarToComparisonValue(NodeType type) { if (type == NodeType.REGEX_PERMISSION) { - return Constraint.of(StandardComparison.SIMILAR, RegexPermission.key(StandardComparison.WILDCARD)); + return RegexPermission.key(Comparison.WILDCARD); } else if (type == NodeType.INHERITANCE) { - return Constraint.of(StandardComparison.SIMILAR, Inheritance.key(StandardComparison.WILDCARD)); + return Inheritance.key(Comparison.WILDCARD); } else if (type == NodeType.PREFIX) { - return Constraint.of(StandardComparison.SIMILAR, Prefix.NODE_MARKER + StandardComparison.WILDCARD + AbstractNode.NODE_SEPARATOR + StandardComparison.WILDCARD); + return Prefix.NODE_MARKER + Comparison.WILDCARD + AbstractNode.NODE_SEPARATOR + Comparison.WILDCARD; } else if (type == NodeType.SUFFIX) { - return Constraint.of(StandardComparison.SIMILAR, Suffix.NODE_MARKER + StandardComparison.WILDCARD + AbstractNode.NODE_SEPARATOR + StandardComparison.WILDCARD); + return Suffix.NODE_MARKER + Comparison.WILDCARD + AbstractNode.NODE_SEPARATOR + Comparison.WILDCARD; } else if (type == NodeType.META) { - return Constraint.of(StandardComparison.SIMILAR, Meta.key(StandardComparison.WILDCARD, StandardComparison.WILDCARD)); + return Meta.key(Comparison.WILDCARD, Comparison.WILDCARD); } else if (type == NodeType.WEIGHT) { - return Constraint.of(StandardComparison.SIMILAR, Weight.NODE_MARKER + StandardComparison.WILDCARD); + return Weight.NODE_MARKER + Comparison.WILDCARD; } else if (type == NodeType.DISPLAY_NAME) { - return Constraint.of(StandardComparison.SIMILAR, DisplayName.key(StandardComparison.WILDCARD)); + return DisplayName.key(Comparison.WILDCARD); } throw new IllegalArgumentException("Unable to create a NodeMatcher for NodeType " + type.name()); diff --git a/common/src/main/java/me/lucko/luckperms/common/node/types/DisplayName.java b/common/src/main/java/me/lucko/luckperms/common/node/types/DisplayName.java index 221bd0efd..fe87b1a9d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/types/DisplayName.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/types/DisplayName.java @@ -27,14 +27,13 @@ import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.AbstractNodeBuilder; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.node.types.DisplayNameNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -72,7 +71,7 @@ public DisplayName(String displayName, boolean value, long expireAt, ImmutableCo } public static @Nullable Builder parse(String key) { - if (!key.toLowerCase().startsWith(NODE_MARKER)) { + if (!key.toLowerCase(Locale.ROOT).startsWith(NODE_MARKER)) { return null; } @@ -94,7 +93,11 @@ public Builder(String displayName, boolean value, long expireAt, ImmutableContex @Override public @NonNull Builder displayName(@NonNull String displayName) { - this.displayName = Objects.requireNonNull(displayName, "displayName"); + Objects.requireNonNull(displayName, "displayName"); + if (displayName.isEmpty()) { + throw new IllegalArgumentException("display name is empty"); + } + this.displayName = displayName; return this; } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/types/Inheritance.java b/common/src/main/java/me/lucko/luckperms/common/node/types/Inheritance.java index ab8c7191b..d19128022 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/types/Inheritance.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/types/Inheritance.java @@ -27,15 +27,15 @@ import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.AbstractNodeBuilder; - +import me.lucko.luckperms.common.storage.misc.DataConstraints; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.group.Group; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.node.types.InheritanceNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -44,7 +44,7 @@ public class Inheritance extends AbstractNode, Object> metadata) { super(key(groupName), value, expireAt, contexts, metadata); - this.groupName = groupName.toLowerCase(); + this.groupName = groupName.toLowerCase(Locale.ROOT); } @Override @@ -73,7 +73,7 @@ public Inheritance(String groupName, boolean value, long expireAt, ImmutableCont } public static @Nullable Builder parse(String key) { - key = key.toLowerCase(); + key = key.toLowerCase(Locale.ROOT); if (!key.startsWith(NODE_MARKER)) { return null; } @@ -96,7 +96,11 @@ public Builder(String groupName, boolean value, long expireAt, ImmutableContextS @Override public @NonNull Builder group(@NonNull String group) { - this.groupName = Objects.requireNonNull(group, "group"); + Objects.requireNonNull(group, "group"); + if (!DataConstraints.GROUP_NAME_TEST.test(group)) { + throw new IllegalArgumentException("group name is invalid"); + } + this.groupName = group; return this; } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/types/Meta.java b/common/src/main/java/me/lucko/luckperms/common/node/types/Meta.java index 243285282..f5477088e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/types/Meta.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/types/Meta.java @@ -28,15 +28,14 @@ import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.AbstractNodeBuilder; import me.lucko.luckperms.common.node.factory.Delimiters; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.node.types.MetaNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Iterator; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -45,7 +44,7 @@ public class Meta extends AbstractNode implements Me private static final String NODE_MARKER = NODE_KEY + "."; public static String key(String key, String value) { - return NODE_MARKER + Delimiters.escapeCharacters(key).toLowerCase() + AbstractNode.NODE_SEPARATOR + Delimiters.escapeCharacters(value); + return NODE_MARKER + Delimiters.escapeCharacters(key).toLowerCase(Locale.ROOT) + AbstractNode.NODE_SEPARATOR + Delimiters.escapeCharacters(value); } public static Builder builder() { @@ -61,7 +60,7 @@ public static Builder builder(String key, String value) { public Meta(String metaKey, String metaValue, boolean value, long expireAt, ImmutableContextSet contexts, Map, Object> metadata) { super(key(metaKey, metaValue), value, expireAt, contexts, metadata); - this.metaKey = metaKey.toLowerCase(); + this.metaKey = metaKey.toLowerCase(Locale.ROOT); this.metaValue = metaValue; } @@ -81,7 +80,7 @@ public Meta(String metaKey, String metaValue, boolean value, long expireAt, Immu } public static @Nullable Builder parse(String key) { - if (!key.toLowerCase().startsWith(NODE_MARKER)) { + if (!key.toLowerCase(Locale.ROOT).startsWith(NODE_MARKER)) { return null; } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/types/Permission.java b/common/src/main/java/me/lucko/luckperms/common/node/types/Permission.java index 8c62b021b..31357b33b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/types/Permission.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/types/Permission.java @@ -29,12 +29,10 @@ import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.AbstractNodeBuilder; import me.lucko.luckperms.common.node.factory.NodeBuilders; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.node.types.PermissionNode; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Map; diff --git a/common/src/main/java/me/lucko/luckperms/common/node/types/Prefix.java b/common/src/main/java/me/lucko/luckperms/common/node/types/Prefix.java index 1408466ff..3902afae2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/types/Prefix.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/types/Prefix.java @@ -28,16 +28,15 @@ import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.AbstractNodeBuilder; import me.lucko.luckperms.common.node.factory.Delimiters; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.ChatMetaType; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.node.types.PrefixNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Iterator; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -87,7 +86,7 @@ public int getPriority() { } public static @Nullable Builder parse(String key) { - if (!key.toLowerCase().startsWith(NODE_MARKER)) { + if (!key.toLowerCase(Locale.ROOT).startsWith(NODE_MARKER)) { return null; } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/types/RegexPermission.java b/common/src/main/java/me/lucko/luckperms/common/node/types/RegexPermission.java index e8c7df816..c04c943a6 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/types/RegexPermission.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/types/RegexPermission.java @@ -29,11 +29,9 @@ import me.lucko.luckperms.common.cache.PatternCache; import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.AbstractNodeBuilder; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.node.types.RegexPermissionNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/node/types/Suffix.java b/common/src/main/java/me/lucko/luckperms/common/node/types/Suffix.java index dd78fcd0e..4821c0513 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/types/Suffix.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/types/Suffix.java @@ -28,16 +28,15 @@ import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.AbstractNodeBuilder; import me.lucko.luckperms.common.node.factory.Delimiters; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.ChatMetaType; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.node.types.SuffixNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Iterator; +import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -87,7 +86,7 @@ public int getPriority() { } public static @Nullable Builder parse(String key) { - if (!key.toLowerCase().startsWith(NODE_MARKER)) { + if (!key.toLowerCase(Locale.ROOT).startsWith(NODE_MARKER)) { return null; } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/types/Weight.java b/common/src/main/java/me/lucko/luckperms/common/node/types/Weight.java index 13773d56c..4e6c02085 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/types/Weight.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/types/Weight.java @@ -27,14 +27,13 @@ import me.lucko.luckperms.common.node.AbstractNode; import me.lucko.luckperms.common.node.AbstractNodeBuilder; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.node.types.WeightNode; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Locale; import java.util.Map; public class Weight extends AbstractNode implements WeightNode { @@ -71,7 +70,7 @@ public int getWeight() { } public static @Nullable Builder parse(String key) { - if (!key.toLowerCase().startsWith(NODE_MARKER)) { + if (!key.toLowerCase(Locale.ROOT).startsWith(NODE_MARKER)) { return null; } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/utils/NodeJsonSerializer.java b/common/src/main/java/me/lucko/luckperms/common/node/utils/NodeJsonSerializer.java index cd98e6b2e..5dc1ffeb1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/utils/NodeJsonSerializer.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/utils/NodeJsonSerializer.java @@ -28,16 +28,16 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; - -import me.lucko.luckperms.common.context.ContextSetJsonSerializer; +import me.lucko.luckperms.common.context.serializer.ContextSetJsonSerializer; import me.lucko.luckperms.common.node.factory.NodeBuilders; - import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeBuilder; +import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; import java.time.Instant; import java.util.Collection; -import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Locale; import java.util.Set; public class NodeJsonSerializer { @@ -46,54 +46,81 @@ private NodeJsonSerializer() { } - public static JsonArray serializeNodes(Collection nodes) { - JsonArray arr = new JsonArray(); - for (Node node : nodes) { - JsonObject attributes = new JsonObject(); + public static JsonObject serializeNode(Node node, boolean includeInheritanceOrigin) { + JsonObject attributes = new JsonObject(); - attributes.addProperty("type", node.getType().name().toLowerCase()); - attributes.addProperty("key", node.getKey()); - attributes.addProperty("value", node.getValue()); + attributes.addProperty("type", node.getType().name().toLowerCase(Locale.ROOT)); + attributes.addProperty("key", node.getKey()); + attributes.addProperty("value", node.getValue()); - Instant expiry = node.getExpiry(); - if (expiry != null) { - attributes.addProperty("expiry", expiry.getEpochSecond()); - } + Instant expiry = node.getExpiry(); + if (expiry != null) { + attributes.addProperty("expiry", expiry.getEpochSecond()); + } - if (!node.getContexts().isEmpty()) { - attributes.add("context", ContextSetJsonSerializer.serialize(node.getContexts())); + if (!node.getContexts().isEmpty()) { + attributes.add("context", ContextSetJsonSerializer.serialize(node.getContexts())); + } + + if (includeInheritanceOrigin) { + InheritanceOriginMetadata origin = node.getMetadata(InheritanceOriginMetadata.KEY).orElse(null); + if (origin != null) { + JsonObject metadata = new JsonObject(); + metadata.add("inheritanceOrigin", serializeInheritanceOrigin(origin)); + attributes.add("metadata", metadata); } + } + + return attributes; + } - arr.add(attributes); + private static JsonObject serializeInheritanceOrigin(InheritanceOriginMetadata origin) { + JsonObject obj = new JsonObject(); + obj.addProperty("type", origin.getOrigin().getType()); + obj.addProperty("name", origin.getOrigin().getName()); + return obj; + } + + public static JsonArray serializeNodes(Collection nodes) { + JsonArray arr = new JsonArray(); + for (Node node : nodes) { + arr.add(serializeNode(node, false)); } return arr; } - public static Set deserializeNodes(JsonArray arr) { - Set nodes = new HashSet<>(); - for (JsonElement ent : arr) { - JsonObject attributes = ent.getAsJsonObject(); + public static Node deserializeNode(JsonElement ent) { + JsonObject attributes = ent.getAsJsonObject(); - String key = attributes.get("key").getAsString(); + String key = attributes.get("key").getAsString(); - if (key.isEmpty()) { - continue; // skip - } + if (key.isEmpty()) { + return null; // skip + } - NodeBuilder builder = NodeBuilders.determineMostApplicable(key); + NodeBuilder builder = NodeBuilders.determineMostApplicable(key); - boolean value = attributes.get("value").getAsBoolean(); - builder.value(value); + boolean value = attributes.get("value").getAsBoolean(); + builder.value(value); - if (attributes.has("expiry")) { - builder.expiry(attributes.get("expiry").getAsLong()); - } + if (attributes.has("expiry")) { + builder.expiry(attributes.get("expiry").getAsLong()); + } - if (attributes.has("context")) { - builder.context(ContextSetJsonSerializer.deserialize(attributes.get("context"))); - } + if (attributes.has("context")) { + builder.context(ContextSetJsonSerializer.deserialize(attributes.get("context"))); + } + + return builder.build(); + } - nodes.add(builder.build()); + public static Set deserializeNodes(JsonArray arr) { + Set nodes = new LinkedHashSet<>(arr.size()); + for (JsonElement ent : arr) { + Node node = deserializeNode(ent); + if (node != null) { + nodes.add(node); + } } return nodes; } diff --git a/common/src/main/java/me/lucko/luckperms/common/node/utils/ShorthandParseException.java b/common/src/main/java/me/lucko/luckperms/common/node/utils/ShorthandParseException.java new file mode 100644 index 000000000..3da2a16c9 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/node/utils/ShorthandParseException.java @@ -0,0 +1,32 @@ +/* + * 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.node.utils; + +public class ShorthandParseException extends Exception { + public ShorthandParseException(String msg) { + super(msg); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/node/utils/ShorthandParser.java b/common/src/main/java/me/lucko/luckperms/common/node/utils/ShorthandParser.java index 86c67720d..38d685045 100644 --- a/common/src/main/java/me/lucko/luckperms/common/node/utils/ShorthandParser.java +++ b/common/src/main/java/me/lucko/luckperms/common/node/utils/ShorthandParser.java @@ -29,8 +29,10 @@ import com.google.common.base.Splitter; import com.google.common.collect.Iterators; +import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.NoSuchElementException; import java.util.Set; /** @@ -43,13 +45,21 @@ public enum ShorthandParser { */ NUMERIC_RANGE { @Override - public Iterator extract(String input) throws NumberFormatException { + public Iterator extract(String input) throws ShorthandParseException { int index = input.indexOf(RANGE_SEPARATOR); if (index == -1 || index == 0 || index == input.length() - 1) { return null; } - return new RangeIterator(Integer.parseInt(input.substring(0, index)), Integer.parseInt(input.substring(index + 1))) { + int a, b; + try { + a = Integer.parseInt(input.substring(0, index)); + b = Integer.parseInt(input.substring(index + 1)); + } catch (NumberFormatException e) { + return null; + } + + return new RangeIterator(a, b, false) { @Override protected String toString(int i) { return Integer.toString(i); @@ -63,12 +73,12 @@ protected String toString(int i) { */ CHARACTER_RANGE { @Override - public Iterator extract(String input) { + public Iterator extract(String input) throws ShorthandParseException { if (input.length() != 3 || input.charAt(1) != RANGE_SEPARATOR) { return null; } - return new RangeIterator(input.charAt(0), input.charAt(2)) { + return new RangeIterator(input.charAt(0), input.charAt(2), true) { @Override protected String toString(int i) { return Character.toString((char) i); @@ -97,9 +107,9 @@ public Iterator extract(String input) { * * @param input the input shorthand * @return an iterator of the resultant strings, or optionally null if the input was invalid - * @throws IllegalArgumentException if the input was invalid + * @throws ShorthandParseException if the range is too large or the input is invalid */ - abstract Iterator extract(String input) throws IllegalArgumentException; + abstract Iterator extract(String input) throws ShorthandParseException; /** Character used to open a group */ private static final char OPEN_GROUP = '{'; @@ -119,13 +129,46 @@ public Iterator extract(String input) { /** The parsers */ private static final ShorthandParser[] PARSERS = values(); + /** The max number of results to return */ + private static final int MAX_RESULTS = 1000; + + /** + * Parses and expands the shorthand format. + * + * @param s the string to expand + * @return the expanded result, or an empty set if the input was invalid + */ + public static Set expandShorthandSafely(String s) { + try { + return expandShorthand(s); + } catch (ShorthandParseException e) { + return Collections.emptySet(); + } + } + + /** + * Check if the string can be expanded safely. + * + * @param s the string to expand + * @return the error, if any + */ + public static ShorthandParseException checkParse(String s) { + try { + expandShorthand(s); + return null; + } catch (ShorthandParseException e) { + return e; + } + } + /** * Parses and expands the shorthand format. * * @param s the string to expand * @return the expanded result + * @throws ShorthandParseException if the range is too large or the input is invalid */ - public static Set expandShorthand(String s) { + public static Set expandShorthand(String s) throws ShorthandParseException { Set results = new HashSet<>(); results.add(s); @@ -141,6 +184,10 @@ public static Set expandShorthand(String s) { } else { workSet.add(string); } + + if (workSet.size() > MAX_RESULTS) { + throw new ShorthandParseException("Results exceeded limit of " + MAX_RESULTS + " when parsing '" + string + "'"); + } } if (work) { @@ -162,7 +209,7 @@ public static Set expandShorthand(String s) { return results; } - private static Set matchGroup(String input) { + private static Set matchGroup(String input) throws ShorthandParseException { int openingIndex = indexOfEither(input, OPEN_GROUP, OPEN_GROUP_2); if (openingIndex == -1) { return null; @@ -210,12 +257,20 @@ private static int indexOfEither(String s, char c1, char c2) { * Implements an iterator over a given range of ints. */ private abstract static class RangeIterator implements Iterator { + private static final int MAX_RANGE = 250; + private final int max; - private int next; + private long next; - RangeIterator(int a, int b) { + RangeIterator(int a, int b, boolean characters) throws ShorthandParseException { this.max = Math.max(a, b); this.next = Math.min(a, b); + if ((this.max - this.next) > MAX_RANGE) { + throw new ShorthandParseException(characters + ? "Range between " + (char) a + " and " + (char) b + " exceeds limit of " + MAX_RANGE + : "Range between " + a + " and " + b + " exceeds limit of " + MAX_RANGE + ); + } } protected abstract String toString(int i); @@ -227,7 +282,10 @@ public final boolean hasNext() { @Override public final String next() { - return toString(this.next++); + if (!hasNext()) { + throw new NoSuchElementException(); + } + return toString((int) this.next++); } } 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 63576ab56..57b264680 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 @@ -29,17 +29,25 @@ import me.lucko.luckperms.common.api.ApiRegistrationUtil; import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.LuckPermsConfiguration; import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; -import me.lucko.luckperms.common.context.ConfigurationContextCalculator; +import me.lucko.luckperms.common.config.generic.adapter.EnvironmentVariableConfigAdapter; +import me.lucko.luckperms.common.config.generic.adapter.FileSecretConfigAdapter; +import me.lucko.luckperms.common.config.generic.adapter.MultiConfigurationAdapter; +import me.lucko.luckperms.common.config.generic.adapter.SystemPropertyConfigAdapter; +import me.lucko.luckperms.common.context.calculator.ConfigurationContextCalculator; import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.dependencies.DependencyManager; +import me.lucko.luckperms.common.dependencies.DependencyManagerImpl; +import me.lucko.luckperms.common.dependencies.DependencyRepository; import me.lucko.luckperms.common.event.AbstractEventBus; import me.lucko.luckperms.common.event.EventDispatcher; import me.lucko.luckperms.common.event.gen.GeneratedEventClass; import me.lucko.luckperms.common.extension.SimpleExtensionManager; import me.lucko.luckperms.common.http.BytebinClient; +import me.lucko.luckperms.common.http.BytesocksClient; import me.lucko.luckperms.common.inheritance.InheritanceGraphFactory; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.locale.TranslationManager; @@ -47,17 +55,22 @@ import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.messaging.MessagingFactory; import me.lucko.luckperms.common.plugin.logging.PluginLogger; +import me.lucko.luckperms.common.plugin.util.HealthCheckResult; import me.lucko.luckperms.common.storage.Storage; import me.lucko.luckperms.common.storage.StorageFactory; -import me.lucko.luckperms.common.storage.StorageType; +import me.lucko.luckperms.common.storage.StorageMetadata; import me.lucko.luckperms.common.storage.implementation.file.watcher.FileWatcher; import me.lucko.luckperms.common.storage.misc.DataConstraints; +import me.lucko.luckperms.common.tasks.CacheHousekeepingTask; +import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; import me.lucko.luckperms.common.tasks.SyncTask; +import me.lucko.luckperms.common.treeview.AsyncPermissionRegistry; import me.lucko.luckperms.common.treeview.PermissionRegistry; import me.lucko.luckperms.common.verbose.VerboseHandler; - +import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; +import me.lucko.luckperms.common.webeditor.store.WebEditorStore; import net.luckperms.api.LuckPerms; - +import net.luckperms.api.platform.Health; import okhttp3.OkHttpClient; import java.io.IOException; @@ -68,7 +81,11 @@ import java.time.Instant; import java.time.LocalDate; import java.time.Month; +import java.util.Collections; import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -79,13 +96,16 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { // init during load private DependencyManager dependencyManager; private TranslationManager translationManager; + private AsyncPermissionRegistry permissionRegistry; + private VerboseHandler verboseHandler; // init during enable - private VerboseHandler verboseHandler; - private PermissionRegistry permissionRegistry; private LogDispatcher logDispatcher; private LuckPermsConfiguration configuration; + private OkHttpClient httpClient; private BytebinClient bytebin; + private BytesocksClient bytesocks; + private WebEditorStore webEditorStore; private TranslationRepository translationRepository; private FileWatcher fileWatcher = null; private Storage storage; @@ -97,16 +117,37 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { private EventDispatcher eventDispatcher; private SimpleExtensionManager extensionManager; + private boolean running = false; + /** * Performs the initial actions to load the plugin */ public final void load() { // load dependencies - this.dependencyManager = new DependencyManager(this); + this.dependencyManager = createDependencyManager(); this.dependencyManager.loadDependencies(getGlobalDependencies()); + // load translations this.translationManager = new TranslationManager(this); this.translationManager.reload(); + + // load some utilities early + this.permissionRegistry = new AsyncPermissionRegistry(getBootstrap().getScheduler()); + for (CommandPermission permission : CommandPermission.values()) { + this.permissionRegistry.insert(permission.getPermission()); + } + + this.verboseHandler = new VerboseHandler(getBootstrap().getScheduler()); + + // load configuration + getLogger().info("Loading configuration..."); + ConfigurationAdapter configFileAdapter = provideConfigurationAdapter(); + this.configuration = new LuckPermsConfiguration(this, new MultiConfigurationAdapter(this, + new FileSecretConfigAdapter(this), + new SystemPropertyConfigAdapter(this), + new EnvironmentVariableConfigAdapter(this), + configFileAdapter + )); } public final void enable() { @@ -116,21 +157,25 @@ public final void enable() { // send the startup banner Message.STARTUP_BANNER.send(getConsoleSender(), getBootstrap()); - // load some utilities early - this.verboseHandler = new VerboseHandler(getBootstrap().getScheduler()); - this.permissionRegistry = new PermissionRegistry(getBootstrap().getScheduler()); + // setup log dispatcher instance early this.logDispatcher = new LogDispatcher(this); - // load configuration - getLogger().info("Loading configuration..."); - this.configuration = new LuckPermsConfiguration(this, provideConfigurationAdapter()); - // setup a bytebin instance - OkHttpClient httpClient = new OkHttpClient.Builder() + this.httpClient = new OkHttpClient.Builder() .callTimeout(15, TimeUnit.SECONDS) .build(); - this.bytebin = new BytebinClient(httpClient, getConfiguration().get(ConfigKeys.BYTEBIN_URL), "luckperms"); + this.bytebin = new BytebinClient( + this.httpClient, + getConfiguration().get(ConfigKeys.BYTEBIN_URL), + "luckperms" + ); + this.bytesocks = new BytesocksClient( + this.httpClient, + getConfiguration().get(ConfigKeys.BYTESOCKS_URL), + "luckperms/editor" + ); + this.webEditorStore = new WebEditorStore(this); // init translation repo and update bundle files this.translationRepository = new TranslationRepository(this); @@ -138,8 +183,12 @@ public final void enable() { // now the configuration is loaded, we can create a storage factory and load initial dependencies StorageFactory storageFactory = new StorageFactory(this); - Set storageTypes = storageFactory.getRequiredTypes(); - this.dependencyManager.loadStorageDependencies(storageTypes); + this.dependencyManager.loadStorageDependencies( + storageFactory.getRequiredTypes(), + getConfiguration().get(ConfigKeys.REDIS_ENABLED), + getConfiguration().get(ConfigKeys.RABBITMQ_ENABLED), + getConfiguration().get(ConfigKeys.NATS_ENABLED) + ); // register listeners registerPlatformListeners(); @@ -151,7 +200,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); } } @@ -164,7 +213,11 @@ public final void enable() { this.syncTaskBuffer = new SyncTask.Buffer(this); // register commands - registerCommands(); + if (skipCommandRegistration()) { + getLogger().warn("LuckPerms commands are disabled in the configuration for both console and players. Skipping command registration."); + } else { + registerCommands(); + } // load internal managers getLogger().info("Loading internal permission managers..."); @@ -185,6 +238,7 @@ public final void enable() { // register with the LP API this.apiProvider = new LuckPermsApiProvider(this); + this.apiProvider.ensureApiWasLoadedByPlugin(); this.eventDispatcher = new EventDispatcher(provideEventBus(this.apiProvider)); getBootstrap().getScheduler().executeAsync(GeneratedEventClass::preGenerate); ApiRegistrationUtil.registerProvider(this.apiProvider); @@ -195,9 +249,9 @@ public final void enable() { this.extensionManager.loadExtensions(getBootstrap().getConfigDirectory().resolve("extensions")); // schedule update tasks - int mins = getConfiguration().get(ConfigKeys.SYNC_TIME); - if (mins > 0) { - getBootstrap().getScheduler().asyncRepeating(() -> this.syncTaskBuffer.request(), mins, TimeUnit.MINUTES); + int syncMins = getConfiguration().get(ConfigKeys.SYNC_TIME); + if (syncMins > 0) { + getBootstrap().getScheduler().asyncRepeating(() -> this.syncTaskBuffer.request(), syncMins, TimeUnit.MINUTES); } // run an update instantly. @@ -214,6 +268,9 @@ public final void enable() { // perform any platform-specific final setup tasks performFinalSetup(); + // mark as running + this.running = true; + Duration timeTaken = Duration.between(getBootstrap().getStartupTime(), Instant.now()); getLogger().info("Successfully enabled. (took " + timeTaken.toMillis() + "ms)"); } @@ -224,6 +281,13 @@ public final void disable() { // cancel delayed/repeating tasks getBootstrap().getScheduler().shutdownScheduler(); + // close web editor sockets + for (WebEditorSocket socket : this.webEditorStore.sockets().getSockets()) { + if (!socket.isClosed()) { + socket.close(); + } + } + // shutdown permission vault and verbose handler tasks this.permissionRegistry.close(); this.verboseHandler.close(); @@ -231,6 +295,9 @@ public final void disable() { // unload extensions this.extensionManager.close(); + // mark as not running + this.running = false; + // remove any hooks into the platform removePlatformHooks(); @@ -255,9 +322,25 @@ public final void disable() { // shutdown async executor pool getBootstrap().getScheduler().shutdownExecutor(); + // shutdown okhttp + this.httpClient.dispatcher().executorService().shutdown(); + this.httpClient.connectionPool().evictAll(); + + // close isolated loaders for non-relocated dependencies + getDependencyManager().close(); + + // close classpath appender + getBootstrap().getClassPathAppender().close(); + getLogger().info("Goodbye!"); } + // hooks called during load + + protected DependencyManager createDependencyManager() { + return new DependencyManagerImpl(this, DependencyRepository.REMOTE_MAVEN_REPOSITORIES); + } + protected Set getGlobalDependencies() { return EnumSet.of( Dependency.ADVENTURE, @@ -269,8 +352,34 @@ protected Set getGlobalDependencies() { ); } + // hooks called during enable + + protected void registerHousekeepingTasks() { + getBootstrap().getScheduler().asyncRepeating(new ExpireTemporaryTask(this), 3, TimeUnit.SECONDS); + getBootstrap().getScheduler().asyncRepeating(new CacheHousekeepingTask(this), 2, TimeUnit.MINUTES); + } + + protected abstract void setupSenderFactory(); + protected abstract ConfigurationAdapter provideConfigurationAdapter(); + protected abstract void registerPlatformListeners(); + protected abstract MessagingFactory provideMessagingFactory(); + protected abstract void registerCommands(); + protected abstract void setupManagers(); + protected abstract CalculatorFactory provideCalculatorFactory(); + protected abstract void setupContextManager(); + protected abstract void setupPlatformHooks(); + protected abstract AbstractEventBus provideEventBus(LuckPermsApiProvider apiProvider); + protected abstract void registerApiOnPlatform(LuckPerms api); + protected abstract void performFinalSetup(); + + // hooks called during disable + + protected void removePlatformHooks() {} + protected Path resolveConfig(String fileName) { Path configFile = getBootstrap().getConfigDirectory().resolve(fileName); + + // if the config doesn't exist, create it based on the template in the resources dir if (!Files.exists(configFile)) { try { Files.createDirectories(configFile.getParent()); @@ -284,24 +393,14 @@ protected Path resolveConfig(String fileName) { throw new RuntimeException(e); } } + return configFile; } - protected abstract void setupSenderFactory(); - protected abstract ConfigurationAdapter provideConfigurationAdapter(); - protected abstract void registerPlatformListeners(); - protected abstract MessagingFactory provideMessagingFactory(); - protected abstract void registerCommands(); - protected abstract void setupManagers(); - protected abstract CalculatorFactory provideCalculatorFactory(); - protected abstract void setupContextManager(); - protected abstract void setupPlatformHooks(); - protected abstract AbstractEventBus provideEventBus(LuckPermsApiProvider apiProvider); - protected abstract void registerApiOnPlatform(LuckPerms api); - protected abstract void registerHousekeepingTasks(); - protected abstract void performFinalSetup(); - - protected void removePlatformHooks() {} + protected boolean skipCommandRegistration() { + return getConfiguration().get(ConfigKeys.DISABLE_LUCKPERMS_COMMANDS_CONSOLE) && + getConfiguration().get(ConfigKeys.DISABLE_LUCKPERMS_COMMANDS_PLAYERS); + } @Override public PluginLogger getLogger() { @@ -315,10 +414,35 @@ public void setMessagingService(InternalMessagingService messagingService) { } } + @Override + public Health runHealthCheck() { + if (!this.running) { + return HealthCheckResult.unhealthy(Collections.emptyMap()); + } + + StorageMetadata meta = this.storage.getMeta(); + if (meta.connected() != null && !meta.connected()) { + return HealthCheckResult.unhealthy(Collections.singletonMap("reason", "storage disconnected")); + } + + Map map = new LinkedHashMap<>(); + if (meta.connected() != null) { + map.put("storageConnected", meta.connected()); + } + if (meta.ping() != null) { + map.put("storagePing", meta.ping()); + } + if (meta.sizeBytes() != null) { + map.put("storageSizeBytes", meta.sizeBytes()); + } + + return HealthCheckResult.healthy(map); + } + @Override public Optional lookupUniqueId(String username) { // get a result from the DB cache - UUID uniqueId = getStorage().getPlayerUniqueId(username.toLowerCase()).join(); + UUID uniqueId = getStorage().getPlayerUniqueId(username.toLowerCase(Locale.ROOT)).join(); // fire the event uniqueId = getEventDispatcher().dispatchUniqueIdLookup(username, uniqueId); @@ -392,11 +516,25 @@ public LuckPermsConfiguration getConfiguration() { return this.configuration; } + public OkHttpClient getHttpClient() { + return this.httpClient; + } + @Override public BytebinClient getBytebin() { return this.bytebin; } + @Override + public BytesocksClient getBytesocks() { + return this.bytesocks; + } + + @Override + public WebEditorStore getWebEditorStore() { + return this.webEditorStore; + } + @Override public TranslationRepository getTranslationRepository() { return this.translationRepository; diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java index d002d2ece..3bb51a8ca 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java @@ -31,11 +31,12 @@ import me.lucko.luckperms.common.command.CommandManager; import me.lucko.luckperms.common.command.abstraction.Command; import me.lucko.luckperms.common.config.LuckPermsConfiguration; -import me.lucko.luckperms.common.context.ContextManager; +import me.lucko.luckperms.common.context.manager.ContextManager; import me.lucko.luckperms.common.dependencies.DependencyManager; import me.lucko.luckperms.common.event.EventDispatcher; import me.lucko.luckperms.common.extension.SimpleExtensionManager; import me.lucko.luckperms.common.http.BytebinClient; +import me.lucko.luckperms.common.http.BytesocksClient; import me.lucko.luckperms.common.inheritance.InheritanceGraphFactory; import me.lucko.luckperms.common.locale.TranslationManager; import me.lucko.luckperms.common.locale.TranslationRepository; @@ -55,7 +56,8 @@ import me.lucko.luckperms.common.tasks.SyncTask; import me.lucko.luckperms.common.treeview.PermissionRegistry; import me.lucko.luckperms.common.verbose.VerboseHandler; - +import me.lucko.luckperms.common.webeditor.store.WebEditorStore; +import net.luckperms.api.platform.Health; import net.luckperms.api.query.QueryOptions; import java.util.Collections; @@ -248,6 +250,27 @@ public interface LuckPermsPlugin { */ BytebinClient getBytebin(); + /** + * Gets the bytesocks instance in use by platform. + * + * @return the bytesocks instance + */ + BytesocksClient getBytesocks(); + + /** + * Gets the web editor store + * + * @return the web editor store + */ + WebEditorStore getWebEditorStore(); + + /** + * Runs a health check for the plugin. + * + * @return the result of the healthcheck + */ + Health runHealthCheck(); + /** * Gets a calculated context instance for the user using the rules of the platform. * diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/BootstrappedWithLoader.java b/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/BootstrappedWithLoader.java new file mode 100644 index 000000000..d5cdb6f65 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/BootstrappedWithLoader.java @@ -0,0 +1,40 @@ +/* + * 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.plugin.bootstrap; + +/** + * A {@link LuckPermsBootstrap} that was bootstrapped by a loader. + */ +public interface BootstrappedWithLoader { + + /** + * Gets the loader object that did the bootstrapping. + * + * @return the loader + */ + Object getLoader(); + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java b/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java index 2680ab00e..def95b260 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/bootstrap/LuckPermsBootstrap.java @@ -28,9 +28,7 @@ import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; import me.lucko.luckperms.common.plugin.logging.PluginLogger; import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; - import net.luckperms.api.platform.Platform; - import org.checkerframework.checker.nullness.qual.Nullable; import java.io.InputStream; @@ -135,10 +133,12 @@ public interface LuckPermsBootstrap { /** * Gets the plugins main data storage directory * - *

    Bukkit: /root/plugins/LuckPerms

    - *

    Bungee: /root/plugins/LuckPerms

    - *

    Sponge: /root/luckperms/

    - *

    Fabric: /root/mods/LuckPerms

    + *

    Bukkit: ./plugins/LuckPerms

    + *

    BungeeCord: ./plugins/LuckPerms

    + *

    Sponge: ./luckperms/

    + *

    Velocity: ./plugins/luckperms

    + *

    Fabric: ./mods/LuckPerms

    + *

    Forge: ./config/luckperms

    * * @return the platforms data folder */ diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ClassPathAppender.java b/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ClassPathAppender.java index 5adf9690c..77fefcb14 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ClassPathAppender.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ClassPathAppender.java @@ -30,8 +30,12 @@ /** * Interface which allows access to add URLs to the plugin classpath at runtime. */ -public interface ClassPathAppender { +public interface ClassPathAppender extends AutoCloseable { void addJarToClasspath(Path file); + @Override + default void close() { + + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/JarInJarClassPathAppender.java b/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/JarInJarClassPathAppender.java index 9dd74ea63..0c79e8e3f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/JarInJarClassPathAppender.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/JarInJarClassPathAppender.java @@ -27,6 +27,7 @@ import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import java.io.IOException; import java.net.MalformedURLException; import java.nio.file.Path; @@ -40,6 +41,10 @@ public JarInJarClassPathAppender(ClassLoader classLoader) { this.classLoader = (JarInJarClassLoader) classLoader; } + public JarInJarClassLoader getClassLoader() { + return this.classLoader; + } + @Override public void addJarToClasspath(Path file) { try { @@ -48,4 +53,14 @@ public void addJarToClasspath(Path file) { throw new RuntimeException(e); } } + + @Override + public void close() { + this.classLoader.deleteJarResource(); + try { + this.classLoader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ReflectionClassPathAppender.java b/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ReflectionClassPathAppender.java deleted file mode 100644 index ebb175227..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/classpath/ReflectionClassPathAppender.java +++ /dev/null @@ -1,98 +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.common.plugin.classpath; - -import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; - -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Path; - -@Deprecated // TODO: no longer works on Java 16 - Sponge needs to switch to JarInJar or find an API to add to classpath at runtime -public class ReflectionClassPathAppender implements ClassPathAppender { - private static final Method ADD_URL_METHOD; - - static { - // If on Java 9+, open the URLClassLoader module to this module - // so we can access its API via reflection without producing a warning. - try { - openUrlClassLoaderModule(); - } catch (Throwable e) { - // ignore exception - will throw on Java 8 since the Module classes don't exist - } - - // Get the protected 'addURL' method on URLClassLoader and set it to accessible. - try { - ADD_URL_METHOD = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - ADD_URL_METHOD.setAccessible(true); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - private final URLClassLoader classLoader; - - public ReflectionClassPathAppender(LuckPermsBootstrap bootstrap) throws IllegalStateException { - ClassLoader classLoader = bootstrap.getClass().getClassLoader(); - if (classLoader instanceof URLClassLoader) { - this.classLoader = (URLClassLoader) classLoader; - } else { - throw new IllegalStateException("ClassLoader is not instance of URLClassLoader"); - } - } - - @Override - public void addJarToClasspath(Path file) { - try { - ADD_URL_METHOD.invoke(this.classLoader, file.toUri().toURL()); - } catch (ReflectiveOperationException | MalformedURLException e) { - throw new RuntimeException(e); - } - } - - @SuppressWarnings("JavaReflectionMemberAccess") - private static void openUrlClassLoaderModule() throws Exception { - // This is effectively calling: - // - // URLClassLoader.class.getModule().addOpens( - // URLClassLoader.class.getPackageName(), - // ReflectionClassLoader.class.getModule() - // ); - // - // We use reflection since we build against Java 8. - - Class moduleClass = Class.forName("java.lang.Module"); - Method getModuleMethod = Class.class.getMethod("getModule"); - Method addOpensMethod = moduleClass.getMethod("addOpens", String.class, moduleClass); - - Object urlClassLoaderModule = getModuleMethod.invoke(URLClassLoader.class); - Object thisModule = getModuleMethod.invoke(ReflectionClassPathAppender.class); - - addOpensMethod.invoke(urlClassLoaderModule, URLClassLoader.class.getPackage().getName(), thisModule); - } -} diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/AbstractJavaScheduler.java b/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/AbstractJavaScheduler.java deleted file mode 100644 index 2e57b9e11..000000000 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/AbstractJavaScheduler.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.common.plugin.scheduler; - -import com.google.common.util.concurrent.ThreadFactoryBuilder; - -import org.checkerframework.checker.nullness.qual.NonNull; - -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Abstract implementation of {@link SchedulerAdapter} using a {@link ScheduledExecutorService}. - */ -public abstract class AbstractJavaScheduler implements SchedulerAdapter { - private final ScheduledThreadPoolExecutor scheduler; - private final ErrorReportingExecutor schedulerWorkerPool; - private final ForkJoinPool worker; - - public AbstractJavaScheduler() { - this.scheduler = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("luckperms-scheduler") - .build() - ); - this.scheduler.setRemoveOnCancelPolicy(true); - this.schedulerWorkerPool = new ErrorReportingExecutor(Executors.newCachedThreadPool(new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("luckperms-scheduler-worker-%d") - .build() - )); - this.worker = new ForkJoinPool(32, ForkJoinPool.defaultForkJoinWorkerThreadFactory, (t, e) -> e.printStackTrace(), false); - } - - @Override - public Executor async() { - return this.worker; - } - - @Override - public SchedulerTask asyncLater(Runnable task, long delay, TimeUnit unit) { - ScheduledFuture future = this.scheduler.schedule(() -> this.schedulerWorkerPool.execute(task), delay, unit); - return () -> future.cancel(false); - } - - @Override - public SchedulerTask asyncRepeating(Runnable task, long interval, TimeUnit unit) { - ScheduledFuture future = this.scheduler.scheduleAtFixedRate(() -> this.schedulerWorkerPool.execute(task), interval, interval, unit); - return () -> future.cancel(false); - } - - @Override - public void shutdownScheduler() { - this.scheduler.shutdown(); - try { - this.scheduler.awaitTermination(1, TimeUnit.MINUTES); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - @Override - public void shutdownExecutor() { - this.schedulerWorkerPool.delegate.shutdown(); - try { - this.schedulerWorkerPool.delegate.awaitTermination(1, TimeUnit.MINUTES); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - private static final class ErrorReportingExecutor implements Executor { - private final ExecutorService delegate; - - private ErrorReportingExecutor(ExecutorService delegate) { - this.delegate = delegate; - } - - @Override - public void execute(@NonNull Runnable command) { - this.delegate.execute(new ErrorReportingRunnable(command)); - } - } - - private static final class ErrorReportingRunnable implements Runnable { - private final Runnable delegate; - - private ErrorReportingRunnable(Runnable delegate) { - this.delegate = delegate; - } - - @Override - public void run() { - try { - this.delegate.run(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } -} diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/JavaSchedulerAdapter.java b/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/JavaSchedulerAdapter.java new file mode 100644 index 000000000..65943b2cc --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/JavaSchedulerAdapter.java @@ -0,0 +1,139 @@ +/* + * 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.plugin.scheduler; + +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.Arrays; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinWorkerThread; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Abstract implementation of {@link SchedulerAdapter} using a {@link ScheduledExecutorService}. + */ +public class JavaSchedulerAdapter implements SchedulerAdapter { + private static final int PARALLELISM = 16; + + private final LuckPermsBootstrap bootstrap; + + private final ScheduledThreadPoolExecutor scheduler; + private final ForkJoinPool worker; + + public JavaSchedulerAdapter(LuckPermsBootstrap bootstrap) { + this.bootstrap = bootstrap; + + this.scheduler = new ScheduledThreadPoolExecutor(1, r -> { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setName("luckperms-scheduler"); + return thread; + }); + this.scheduler.setRemoveOnCancelPolicy(true); + this.scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + this.worker = new ForkJoinPool(PARALLELISM, new WorkerThreadFactory(), new ExceptionHandler(), false); + } + + @Override + public Executor async() { + return this.worker; + } + + @Override + public SchedulerTask asyncLater(Runnable task, long delay, TimeUnit unit) { + ScheduledFuture future = this.scheduler.schedule(() -> this.worker.execute(task), delay, unit); + return () -> future.cancel(false); + } + + @Override + public SchedulerTask asyncRepeating(Runnable task, long interval, TimeUnit unit) { + ScheduledFuture future = this.scheduler.scheduleAtFixedRate(() -> this.worker.execute(task), interval, interval, unit); + return () -> future.cancel(false); + } + + @Override + public void shutdownScheduler() { + this.scheduler.shutdown(); + try { + if (!this.scheduler.awaitTermination(1, TimeUnit.MINUTES)) { + this.bootstrap.getPluginLogger().severe("Timed out waiting for the LuckPerms scheduler to terminate"); + reportRunningTasks(thread -> thread.getName().equals("luckperms-scheduler")); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Override + public void shutdownExecutor() { + this.worker.shutdown(); + try { + if (!this.worker.awaitTermination(1, TimeUnit.MINUTES)) { + this.bootstrap.getPluginLogger().severe("Timed out waiting for the LuckPerms worker thread pool to terminate"); + reportRunningTasks(thread -> thread.getName().startsWith("luckperms-worker-")); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private void reportRunningTasks(Predicate predicate) { + Thread.getAllStackTraces().forEach((thread, stack) -> { + if (predicate.test(thread)) { + this.bootstrap.getPluginLogger().warn("Thread " + thread.getName() + " is blocked, and may be the reason for the slow shutdown!\n" + + Arrays.stream(stack).map(el -> " " + el).collect(Collectors.joining("\n")) + ); + } + }); + } + + private static final class WorkerThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory { + private static final AtomicInteger COUNT = new AtomicInteger(0); + + @Override + public ForkJoinWorkerThread newThread(ForkJoinPool pool) { + ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + thread.setDaemon(true); + thread.setName("luckperms-worker-" + COUNT.getAndIncrement()); + return thread; + } + } + + private final class ExceptionHandler implements UncaughtExceptionHandler { + @Override + public void uncaughtException(Thread t, Throwable e) { + JavaSchedulerAdapter.this.bootstrap.getPluginLogger().warn("Thread " + t.getName() + " threw an uncaught exception", e); + } + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/SchedulerAdapter.java b/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/SchedulerAdapter.java index 1be23bfbc..34939587f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/SchedulerAdapter.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/scheduler/SchedulerAdapter.java @@ -25,6 +25,8 @@ package me.lucko.luckperms.common.plugin.scheduler; +import me.lucko.luckperms.common.sender.Sender; + import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -40,13 +42,6 @@ public interface SchedulerAdapter { */ Executor async(); - /** - * Gets a sync executor instance - * - * @return a sync executor instance - */ - Executor sync(); - /** * Executes a task async * @@ -61,8 +56,8 @@ default void executeAsync(Runnable task) { * * @param task the task */ - default void executeSync(Runnable task) { - sync().execute(task); + default void executeSync(Sender ctx, Runnable task) { + executeAsync(task); } /** diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/util/AbstractConnectionListener.java b/common/src/main/java/me/lucko/luckperms/common/plugin/util/AbstractConnectionListener.java index ea226dbe2..e78805637 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/util/AbstractConnectionListener.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/util/AbstractConnectionListener.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.model.data.DataType; import net.luckperms.api.platform.Platform; @@ -76,7 +75,7 @@ public User loadUser(UUID uniqueId, String username) { // most likely because ip forwarding is not setup correctly // print a warning to the console - if (saveResult.includes(PlayerSaveResult.Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME)) { + if (saveResult.includes(PlayerSaveResult.Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME) && !Boolean.getBoolean("luckperms.suppress-uuid-mismatch-warning")) { Set otherUuids = saveResult.getOtherUniqueIds(); this.plugin.getLogger().warn("LuckPerms already has data for player '" + username + "' - but this data is stored under a different UUID."); diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/util/HealthCheckResult.java b/common/src/main/java/me/lucko/luckperms/common/plugin/util/HealthCheckResult.java new file mode 100644 index 000000000..2b080ea96 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/util/HealthCheckResult.java @@ -0,0 +1,67 @@ +/* + * 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.plugin.util; + +import com.google.gson.Gson; +import net.luckperms.api.platform.Health; + +import java.util.Map; + +public class HealthCheckResult implements Health { + private static final Gson GSON = new Gson(); + + public static HealthCheckResult healthy(Map details) { + return new HealthCheckResult(true, details); + } + + public static HealthCheckResult unhealthy(Map details) { + return new HealthCheckResult(false, details); + } + + private final boolean healthy; + private final Map details; + + HealthCheckResult(boolean healthy, Map details) { + this.healthy = healthy; + this.details = details; + } + + @Override + public boolean isHealthy() { + return this.healthy; + } + + @Override + public Map getDetails() { + return this.details; + } + + @Override + public String toString() { + return GSON.toJson(this); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/query/FlagUtils.java b/common/src/main/java/me/lucko/luckperms/common/query/FlagUtils.java index 6b0be4495..9a6766e31 100644 --- a/common/src/main/java/me/lucko/luckperms/common/query/FlagUtils.java +++ b/common/src/main/java/me/lucko/luckperms/common/query/FlagUtils.java @@ -54,7 +54,7 @@ static byte toByte(Set settings) { private static byte toByte0(Set settings) { byte b = 0; for (Flag setting : settings) { - b |= 1 << setting.ordinal(); + b |= (byte) (1 << setting.ordinal()); } return b; } diff --git a/common/src/main/java/me/lucko/luckperms/common/query/QueryOptionsBuilderImpl.java b/common/src/main/java/me/lucko/luckperms/common/query/QueryOptionsBuilderImpl.java index 539a97432..3ff75aa63 100644 --- a/common/src/main/java/me/lucko/luckperms/common/query/QueryOptionsBuilderImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/query/QueryOptionsBuilderImpl.java @@ -25,15 +25,13 @@ package me.lucko.luckperms.common.query; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; - +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.query.Flag; import net.luckperms.api.query.OptionKey; import net.luckperms.api.query.QueryMode; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/query/QueryOptionsImpl.java b/common/src/main/java/me/lucko/luckperms/common/query/QueryOptionsImpl.java index 7eab66e99..0fe3d6d7e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/query/QueryOptionsImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/query/QueryOptionsImpl.java @@ -27,9 +27,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; - -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; - +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import net.luckperms.api.context.ContextSatisfyMode; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; @@ -37,7 +35,6 @@ import net.luckperms.api.query.OptionKey; import net.luckperms.api.query.QueryMode; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -59,7 +56,7 @@ public class QueryOptionsImpl implements QueryOptions { // computed based on state above private final int hashCode; private Set flagsSet = null; - private final ContextSatisfyMode contextSatisfyMode; + private final ContextSatisfyMode overrideContextSatisfyMode; QueryOptionsImpl(QueryMode mode, @Nullable ImmutableContextSet context, byte flags, @Nullable Map, Object> options) { this.mode = mode; @@ -68,7 +65,7 @@ public class QueryOptionsImpl implements QueryOptions { this.options = options == null ? null : ImmutableMap.copyOf(options); this.hashCode = calculateHashCode(); - this.contextSatisfyMode = options == null ? null : (ContextSatisfyMode) options.get(ContextSatisfyMode.KEY); + this.overrideContextSatisfyMode = options == null ? null : (ContextSatisfyMode) options.get(ContextSatisfyMode.KEY); } @Override @@ -122,7 +119,7 @@ public boolean flag(@NonNull Flag flag) { public boolean satisfies(@NonNull ContextSet contextSet, @NonNull ContextSatisfyMode defaultContextSatisfyMode) { switch (this.mode) { case CONTEXTUAL: - return contextSet.isSatisfiedBy(this.context, this.contextSatisfyMode == null ? defaultContextSatisfyMode : this.contextSatisfyMode); + return contextSet.isSatisfiedBy(this.context, this.overrideContextSatisfyMode == null ? defaultContextSatisfyMode : this.overrideContextSatisfyMode); case NON_CONTEXTUAL: return true; default: 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 63e8601c6..71f541bb0 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 @@ -26,10 +26,11 @@ package me.lucko.luckperms.common.sender; import com.google.common.collect.Iterables; - +import me.lucko.luckperms.common.command.access.CommandPermission; +import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.TextComponent; import net.luckperms.api.util.Tristate; @@ -68,6 +69,10 @@ public LuckPermsPlugin getPlugin() { return this.plugin; } + public T getSender() { + return this.sender; + } + @Override public UUID getUniqueId() { return this.uniqueId; @@ -80,7 +85,7 @@ public String getName() { @Override public void sendMessage(Component message) { - if (isConsole()) { + if (this.factory.shouldSplitNewlines(this.sender)) { for (Component line : splitNewlines(message)) { this.factory.sendMessage(this.sender, line); } @@ -99,6 +104,19 @@ public boolean hasPermission(String permission) { return isConsole() || this.factory.hasPermission(this.sender, permission); } + @Override + public boolean hasPermission(CommandPermission permission) { + boolean readOnlyMode = isConsole() + ? this.plugin.getConfiguration().get(ConfigKeys.READ_ONLY_MODE_CONSOLE) + : this.plugin.getConfiguration().get(ConfigKeys.READ_ONLY_MODE_PLAYERS); + + if (readOnlyMode && !permission.isReadOnly()) { + return false; + } + + return hasPermission(permission.getPermission()); + } + @Override public void performCommand(String commandLine) { this.factory.performCommand(this.sender, commandLine); @@ -130,7 +148,7 @@ public int hashCode() { // A small utility method which splits components built using // > join(newLine(), components...) // back into separate components. - private static Iterable splitNewlines(Component message) { + public static Iterable splitNewlines(Component message) { if (message instanceof TextComponent && message.style().isEmpty() && !message.children().isEmpty() && ((TextComponent) message).content().isEmpty()) { LinkedList> split = new LinkedList<>(); split.add(new ArrayList<>()); @@ -157,7 +175,7 @@ private static Iterable splitNewlines(Component message) { case 1: return input.get(0); default: - return Component.join(Component.empty(), input); + return Component.join(JoinConfiguration.separator(Component.empty()), input); } }); } diff --git a/common/src/main/java/me/lucko/luckperms/common/sender/DummyConsoleSender.java b/common/src/main/java/me/lucko/luckperms/common/sender/DummyConsoleSender.java index d92c37b7d..93d2498f4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/sender/DummyConsoleSender.java +++ b/common/src/main/java/me/lucko/luckperms/common/sender/DummyConsoleSender.java @@ -25,8 +25,9 @@ package me.lucko.luckperms.common.sender; +import me.lucko.luckperms.common.command.access.CommandPermission; +import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.util.Tristate; import java.util.UUID; @@ -48,6 +49,16 @@ public boolean hasPermission(String permission) { return true; } + @Override + public boolean hasPermission(CommandPermission permission) { + boolean readOnlyMode = this.platform.getConfiguration().get(ConfigKeys.READ_ONLY_MODE_CONSOLE); + if (readOnlyMode && !permission.isReadOnly()) { + return false; + } + + return true; + } + @Override public void performCommand(String commandLine) { diff --git a/common/src/main/java/me/lucko/luckperms/common/sender/Sender.java b/common/src/main/java/me/lucko/luckperms/common/sender/Sender.java index 92d177e15..9aa77f557 100644 --- a/common/src/main/java/me/lucko/luckperms/common/sender/Sender.java +++ b/common/src/main/java/me/lucko/luckperms/common/sender/Sender.java @@ -26,9 +26,8 @@ package me.lucko.luckperms.common.sender; import me.lucko.luckperms.common.command.access.CommandPermission; -import me.lucko.luckperms.common.context.ContextManager; +import me.lucko.luckperms.common.context.manager.ContextManager; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.kyori.adventure.text.Component; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; @@ -133,9 +132,7 @@ default String getNameWithLocation() { * @param permission the permission to check for * @return true if the sender has the permission */ - default boolean hasPermission(CommandPermission permission) { - return hasPermission(permission.getPermission()); - } + boolean hasPermission(CommandPermission permission); /** * Makes the sender perform a command. diff --git a/common/src/main/java/me/lucko/luckperms/common/sender/SenderFactory.java b/common/src/main/java/me/lucko/luckperms/common/sender/SenderFactory.java index 797e2b6fe..5c020afdd 100644 --- a/common/src/main/java/me/lucko/luckperms/common/sender/SenderFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/sender/SenderFactory.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.sender; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.kyori.adventure.text.Component; import net.luckperms.api.util.Tristate; @@ -64,6 +63,10 @@ protected P getPlugin() { protected abstract boolean isConsole(T sender); + protected boolean shouldSplitNewlines(T sender) { + return isConsole(sender); + } + public final Sender wrap(T sender) { Objects.requireNonNull(sender, "sender"); return new AbstractSender<>(this.plugin, this, sender); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java b/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java index 9cfacbb89..0f89eecb7 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/Storage.java @@ -26,9 +26,10 @@ package me.lucko.luckperms.common.storage; import com.google.common.collect.ImmutableList; - -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; @@ -37,34 +38,33 @@ import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.implementation.split.SplitStorage; import me.lucko.luckperms.common.storage.misc.NodeEntry; -import me.lucko.luckperms.common.util.Throwing; - -import net.kyori.adventure.text.Component; +import me.lucko.luckperms.common.util.AsyncInterface; import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.event.cause.DeletionCause; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.Node; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; /** * Provides a {@link CompletableFuture} based API for interacting with a {@link StorageImplementation}. */ -public class Storage { +public class Storage extends AsyncInterface { private final LuckPermsPlugin plugin; private final StorageImplementation implementation; public Storage(LuckPermsPlugin plugin, StorageImplementation implementation) { + super(plugin); this.plugin = plugin; this.implementation = implementation; } @@ -81,32 +81,6 @@ public Collection getImplementations() { } } - private CompletableFuture future(Callable supplier) { - return CompletableFuture.supplyAsync(() -> { - try { - return supplier.call(); - } catch (Exception e) { - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } - throw new CompletionException(e); - } - }, this.plugin.getBootstrap().getScheduler().async()); - } - - private CompletableFuture future(Throwing.Runnable runnable) { - return CompletableFuture.runAsync(() -> { - try { - runnable.run(); - } catch (Exception e) { - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } - throw new CompletionException(e); - } - }, this.plugin.getBootstrap().getScheduler().async()); - } - public String getName() { return this.implementation.getImplementationName(); } @@ -127,7 +101,7 @@ public void shutdown() { } } - public Map getMeta() { + public StorageMetadata getMeta() { return this.implementation.getMeta(); } @@ -135,8 +109,8 @@ public CompletableFuture logAction(Action entry) { return future(() -> this.implementation.logAction(entry)); } - public CompletableFuture getLog() { - return future(this.implementation::getLog); + public CompletableFuture getLogPage(FilterList filters, @Nullable PageParameters page) { + return future(() -> this.implementation.getLogPage(filters, page)); } public CompletableFuture applyBulkUpdate(BulkUpdate bulkUpdate) { @@ -153,6 +127,16 @@ public CompletableFuture loadUser(UUID uniqueId, String username) { }); } + public CompletableFuture> loadUsers(Set uniqueIds) { + return future(() -> { + Map users = this.implementation.loadUsers(uniqueIds); + for (User user : users.values()) { + this.plugin.getEventDispatcher().dispatchUserLoad(user); + } + return users; + }); + } + public CompletableFuture saveUser(User user) { return future(() -> this.implementation.saveUser(user)); } @@ -171,7 +155,7 @@ public CompletableFuture>> searchUserNo public CompletableFuture createAndLoadGroup(String name, CreationCause cause) { return future(() -> { - Group group = this.implementation.createAndLoadGroup(name.toLowerCase()); + Group group = this.implementation.createAndLoadGroup(name.toLowerCase(Locale.ROOT)); if (group != null) { this.plugin.getEventDispatcher().dispatchGroupCreate(group, cause); } @@ -181,7 +165,7 @@ public CompletableFuture createAndLoadGroup(String name, CreationCause ca public CompletableFuture> loadGroup(String name) { return future(() -> { - Optional group = this.implementation.loadGroup(name.toLowerCase()); + Optional group = this.implementation.loadGroup(name.toLowerCase(Locale.ROOT)); if (group.isPresent()) { this.plugin.getEventDispatcher().dispatchGroupLoad(group.get()); } @@ -217,7 +201,7 @@ public CompletableFuture>> searchGrou public CompletableFuture createAndLoadTrack(String name, CreationCause cause) { return future(() -> { - Track track = this.implementation.createAndLoadTrack(name.toLowerCase()); + Track track = this.implementation.createAndLoadTrack(name.toLowerCase(Locale.ROOT)); if (track != null) { this.plugin.getEventDispatcher().dispatchTrackCreate(track, cause); } @@ -227,7 +211,7 @@ public CompletableFuture createAndLoadTrack(String name, CreationCause ca public CompletableFuture> loadTrack(String name) { return future(() -> { - Optional track = this.implementation.loadTrack(name.toLowerCase()); + Optional track = this.implementation.loadTrack(name.toLowerCase(Locale.ROOT)); if (track.isPresent()) { this.plugin.getEventDispatcher().dispatchTrackLoad(track.get()); } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java index d07787732..e4c9008e1 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/StorageFactory.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.storage; import com.google.common.collect.ImmutableSet; - import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; @@ -38,6 +37,7 @@ import me.lucko.luckperms.common.storage.implementation.file.loader.TomlLoader; import me.lucko.luckperms.common.storage.implementation.file.loader.YamlLoader; import me.lucko.luckperms.common.storage.implementation.mongodb.MongoStorage; +import me.lucko.luckperms.common.storage.implementation.rest.RestStorage; import me.lucko.luckperms.common.storage.implementation.split.SplitStorage; import me.lucko.luckperms.common.storage.implementation.split.SplitStorageType; import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; @@ -45,7 +45,7 @@ import me.lucko.luckperms.common.storage.implementation.sql.connection.file.SqliteConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MariaDbConnectionFactory; import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.MySqlConnectionFactory; -import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.PostgreConnectionFactory; +import me.lucko.luckperms.common.storage.implementation.sql.connection.hikari.PostgresConnectionFactory; import me.lucko.luckperms.common.util.ImmutableCollectors; import java.util.Map; @@ -114,13 +114,13 @@ private StorageImplementation createNewImplementation(StorageType method) { case H2: return new SqlStorage( this.plugin, - new H2ConnectionFactory(this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-h2")), + new H2ConnectionFactory(this.plugin.getBootstrap().getDataDirectory().resolve("luckperms-h2-v2")), this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX) ); case POSTGRESQL: return new SqlStorage( this.plugin, - new PostgreConnectionFactory(this.plugin.getConfiguration().get(ConfigKeys.DATABASE_VALUES)), + new PostgresConnectionFactory(this.plugin.getConfiguration().get(ConfigKeys.DATABASE_VALUES)), this.plugin.getConfiguration().get(ConfigKeys.SQL_TABLE_PREFIX) ); case MONGODB: @@ -130,6 +130,12 @@ private StorageImplementation createNewImplementation(StorageType method) { this.plugin.getConfiguration().get(ConfigKeys.MONGODB_COLLECTION_PREFIX), this.plugin.getConfiguration().get(ConfigKeys.MONGODB_CONNECTION_URI) ); + case REST: + return new RestStorage( + this.plugin, + this.plugin.getConfiguration().get(ConfigKeys.REST_STORAGE_URL), + this.plugin.getConfiguration().get(ConfigKeys.REST_STORAGE_AUTH_KEY) + ); case YAML: return new SeparatedConfigurateStorage(this.plugin, "YAML", new YamlLoader(), ".yml", "yaml-storage"); case JSON: diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/StorageMetadata.java b/common/src/main/java/me/lucko/luckperms/common/storage/StorageMetadata.java new file mode 100644 index 000000000..2d1bdbd0f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/StorageMetadata.java @@ -0,0 +1,77 @@ +/* + * 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.storage; + +public class StorageMetadata { + + // remote + private Boolean connected; + private Integer ping; + + // local + private Long sizeBytes; + + public Boolean connected() { + return this.connected; + } + + public Integer ping() { + return this.ping; + } + + public Long sizeBytes() { + return this.sizeBytes; + } + + public StorageMetadata connected(boolean connected) { + this.connected = connected; + return this; + } + + public StorageMetadata ping(int ping) { + this.ping = ping; + return this; + } + + public StorageMetadata sizeBytes(long sizeBytes) { + this.sizeBytes = sizeBytes; + return this; + } + + public StorageMetadata combine(StorageMetadata other) { + if (this.connected == null || (other.connected != null && !other.connected)) { + this.connected = other.connected; + } + if (this.ping == null || (other.ping != null && other.ping > this.ping)) { + this.ping = other.ping; + } + if (this.sizeBytes == null || (other.sizeBytes != null && other.sizeBytes > this.sizeBytes)) { + this.sizeBytes = other.sizeBytes; + } + return this; + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/StorageType.java b/common/src/main/java/me/lucko/luckperms/common/storage/StorageType.java index 6973ce4ca..5a8516a17 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/StorageType.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/StorageType.java @@ -51,6 +51,9 @@ public enum StorageType { SQLITE("SQLite", "sqlite"), H2("H2", "h2"), + // REST + REST("REST", "rest"), + // Custom CUSTOM("Custom", "custom"); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java index 56f85b383..7e9f1e315 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/StorageImplementation.java @@ -25,23 +25,22 @@ package me.lucko.luckperms.common.storage.implementation; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; 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.ConstraintNodeMatcher; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.StorageMetadata; import me.lucko.luckperms.common.storage.misc.NodeEntry; - -import net.kyori.adventure.text.Component; import net.luckperms.api.actionlog.Action; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.Node; - import org.checkerframework.checker.nullness.qual.Nullable; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -57,18 +56,18 @@ public interface StorageImplementation { void shutdown(); - default Map getMeta() { - return Collections.emptyMap(); - } + StorageMetadata getMeta(); void logAction(Action entry) throws Exception; - Log getLog() throws Exception; + LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws Exception; void applyBulkUpdate(BulkUpdate bulkUpdate) throws Exception; User loadUser(UUID uniqueId, String username) throws Exception; + Map loadUsers(Set uniqueIds) throws Exception; + void saveUser(User user) throws Exception; Set getUniqueUsers() throws Exception; diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java index 7cfe2934f..2ab38b26f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/AbstractConfigurateStorage.java @@ -26,11 +26,12 @@ package me.lucko.luckperms.common.storage.implementation.file; import com.google.common.collect.Iterables; - -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; -import me.lucko.luckperms.common.context.ContextSetConfigurateSerializer; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.serializer.ContextSetConfigurateSerializer; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.Track; @@ -42,12 +43,12 @@ import me.lucko.luckperms.common.node.types.Prefix; import me.lucko.luckperms.common.node.types.Suffix; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.StorageMetadata; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.implementation.file.loader.ConfigurateLoader; import me.lucko.luckperms.common.storage.implementation.file.loader.JsonLoader; import me.lucko.luckperms.common.storage.implementation.file.loader.YamlLoader; import me.lucko.luckperms.common.util.MoreFiles; - import net.luckperms.api.actionlog.Action; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; @@ -58,15 +59,16 @@ import net.luckperms.api.node.types.ChatMetaNode; import net.luckperms.api.node.types.InheritanceNode; import net.luckperms.api.node.types.MetaNode; - import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.Types; +import org.checkerframework.checker.nullness.qual.Nullable; import java.io.IOException; import java.nio.file.Path; import java.time.Instant; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; @@ -118,6 +120,11 @@ public String getImplementationName() { return this.implementationName; } + @Override + public StorageMetadata getMeta() { + return new StorageMetadata(); + } + /** * Reads a configuration node from the given location * @@ -164,8 +171,8 @@ public void logAction(Action entry) { } @Override - public Log getLog() throws IOException { - return this.actionLogger.getLog(); + public LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws Exception { + return this.actionLogger.getLogPage(filters, page); } @Override @@ -200,6 +207,16 @@ public User loadUser(UUID uniqueId, String username) throws IOException { return user; } + @Override + public Map loadUsers(Set uniqueIds) throws Exception { + // add multithreading here? + Map map = new HashMap<>(); + for (UUID uniqueId : uniqueIds) { + map.put(uniqueId, loadUser(uniqueId, null)); + } + return map; + } + @Override public void saveUser(User user) throws IOException { user.normalData().discardChanges(); @@ -439,11 +456,12 @@ private static NodeEntry parseNode(ConfigurationNode configNode, String keyField } // assume 'configNode' is the actual entry. - String permission = children.get(keyFieldName).getString(null); - if (permission == null || permission.isEmpty()) { + ConfigurationNode appended = children.get(keyFieldName); + if (appended == null) { return null; } + String permission = appended.getString(""); return new NodeEntry(permission, configNode); } @@ -458,7 +476,7 @@ protected static Set readNodes(ConfigurationNode data) { } NodeEntry entry = parseNode(appended, "permission"); - if (entry == null) { + if (entry == null || entry.key.isEmpty()) { continue; } @@ -476,7 +494,7 @@ protected static Set readNodes(ConfigurationNode data) { } NodeEntry entry = parseNode(appended, "group"); - if (entry == null) { + if (entry == null || entry.key.isEmpty()) { continue; } @@ -512,7 +530,7 @@ protected static Set readNodes(ConfigurationNode data) { for (ConfigurationNode appended : data.getNode("meta").getChildrenList()) { NodeEntry entry = parseNode(appended, "key"); - if (entry == null) { + if (entry == null || entry.key.isEmpty()) { continue; } @@ -545,7 +563,7 @@ private static boolean isPlain(Node node) { private void appendNode(ConfigurationNode base, String key, ConfigurationNode attributes, String keyFieldName) { ConfigurationNode appended = base.appendListNode(); - if (this.loader instanceof YamlLoader) { + if (this.loader instanceof YamlLoader && !key.isEmpty()) { // create a map node with a single entry of key --> attributes appended.getNode(key).setValue(attributes); } else { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/CombinedConfigurateStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/CombinedConfigurateStorage.java index aaa58dc1d..491d9586d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/CombinedConfigurateStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/CombinedConfigurateStorage.java @@ -34,9 +34,7 @@ import me.lucko.luckperms.common.storage.misc.NodeEntry; import me.lucko.luckperms.common.util.Iterators; import me.lucko.luckperms.common.util.Uuids; - import net.luckperms.api.node.Node; - import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.loader.ConfigurationLoader; 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 6420d1c51..35ac6c5f3 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 @@ -28,14 +28,16 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.stream.JsonReader; - import me.lucko.luckperms.common.actionlog.ActionJsonSerializer; -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; +import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.cache.BufferedRequest; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.util.gson.GsonProvider; - import net.luckperms.api.actionlog.Action; +import org.checkerframework.checker.nullness.qual.Nullable; import java.io.BufferedReader; import java.io.IOException; @@ -44,11 +46,15 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class FileActionLogger { @@ -132,26 +138,43 @@ public void flush() { } } - public Log getLog() throws IOException { - if (!Files.exists(this.contentFile)) { - return Log.empty(); + private Stream loadLog(FilterList filters) throws IOException { + // if there is log content waiting to be written, flush immediately before trying to read + if (this.saveBuffer.isEnqueued()) { + this.saveBuffer.requestDirectly(); } - Log.Builder log = Log.builder(); + if (!Files.exists(this.contentFile)) { + return Stream.empty(); + } + Stream.Builder builder = Stream.builder(); try (BufferedReader reader = Files.newBufferedReader(this.contentFile, StandardCharsets.UTF_8)) { String line; while ((line = reader.readLine()) != null) { try { JsonElement parsed = GsonProvider.parser().parse(line); - log.add(ActionJsonSerializer.deserialize(parsed)); + LoggedAction action = ActionJsonSerializer.deserialize(parsed); + if (filters.evaluate(action)) { + builder.add(action); + } } catch (Exception e) { e.printStackTrace(); } } } + return builder.build(); + } + + public LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws IOException { + List filtered = loadLog(filters) + .sorted(Comparator.comparing(LoggedAction::getTimestamp)) + .collect(Collectors.toList()); + Collections.reverse(filtered); - return log.build(); + int size = filtered.size(); + List paginated = page != null ? page.paginate(filtered) : filtered; + return LogPage.of(paginated, page, size); } private final class SaveBuffer extends BufferedRequest { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileUuidCache.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileUuidCache.java index e05734753..346a3cbb4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileUuidCache.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/FileUuidCache.java @@ -29,12 +29,9 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Multimaps; import com.google.common.collect.SetMultimap; - import me.lucko.luckperms.common.storage.misc.PlayerSaveResultImpl; import me.lucko.luckperms.common.util.Uuids; - import net.luckperms.api.model.PlayerSaveResult; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -46,6 +43,7 @@ import java.nio.file.Path; import java.util.HashSet; import java.util.Iterator; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -200,11 +198,11 @@ public String put(@NonNull UUID key, @NonNull String value) { // existing might be null if (!value.equalsIgnoreCase(existing)) { if (existing != null) { - this.reverse.remove(existing.toLowerCase(), key); + this.reverse.remove(existing.toLowerCase(Locale.ROOT), key); } } - this.reverse.put(value.toLowerCase(), key); + this.reverse.put(value.toLowerCase(Locale.ROOT), key); return existing; } @@ -213,7 +211,7 @@ public String remove(@NonNull Object k) { UUID key = (UUID) k; String username = super.remove(key); if (username != null) { - this.reverse.remove(username.toLowerCase(), key); + this.reverse.remove(username.toLowerCase(Locale.ROOT), key); } return username; } @@ -223,7 +221,7 @@ public String lookupUsername(UUID uuid) { } public Set lookupUuid(String name) { - return this.reverse.get(name.toLowerCase()); + return this.reverse.get(name.toLowerCase(Locale.ROOT)); } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/SeparatedConfigurateStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/SeparatedConfigurateStorage.java index 047440e89..cf2a2bd3f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/SeparatedConfigurateStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/SeparatedConfigurateStorage.java @@ -27,7 +27,6 @@ import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.bulkupdate.BulkUpdate; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.User; @@ -40,9 +39,7 @@ import me.lucko.luckperms.common.util.Iterators; import me.lucko.luckperms.common.util.MoreFiles; import me.lucko.luckperms.common.util.Uuids; - import net.luckperms.api.node.Node; - import ninja.leaping.configurate.ConfigurationNode; import java.io.IOException; diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/loader/YamlLoader.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/loader/YamlLoader.java index 6bfb11199..eb41a564d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/loader/YamlLoader.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/loader/YamlLoader.java @@ -25,11 +25,10 @@ package me.lucko.luckperms.common.storage.implementation.file.loader; -import org.yaml.snakeyaml.DumperOptions; - import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.loader.ConfigurationLoader; import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; +import org.yaml.snakeyaml.DumperOptions; import java.nio.charset.StandardCharsets; import java.nio.file.Files; diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/watcher/FileWatcher.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/watcher/FileWatcher.java index 4a1ada46a..c5172e9c2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/watcher/FileWatcher.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/file/watcher/FileWatcher.java @@ -98,7 +98,7 @@ public static final class WatchedLocation { private final Path path; /** A set of files which have been modified recently */ - private final Set recentlyModifiedFiles = new ExpiringSet<>(4, TimeUnit.SECONDS); + private final Set recentlyModifiedFiles = ExpiringSet.newExpiringSet(4, TimeUnit.SECONDS); /** The listener callback functions */ private final List> callbacks = new CopyOnWriteArrayList<>(); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java index 05ac6a757..3c2016e45 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorage.java @@ -25,23 +25,31 @@ package me.lucko.luckperms.common.storage.implementation.mongodb; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.mongodb.MongoClient; import com.mongodb.MongoClientOptions; import com.mongodb.MongoClientURI; import com.mongodb.MongoCredential; +import com.mongodb.MongoException; import com.mongodb.ServerAddress; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Indexes; import com.mongodb.client.model.ReplaceOptions; - -import me.lucko.luckperms.common.actionlog.Log; +import com.mongodb.client.model.Sorts; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.Updates; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilterMongoBuilder; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; -import me.lucko.luckperms.common.context.contextset.MutableContextSetImpl; -import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.context.MutableContextSetImpl; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; +import me.lucko.luckperms.common.filter.mongo.ConstraintMongoBuilder; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.common.model.Track; @@ -50,14 +58,14 @@ import me.lucko.luckperms.common.node.factory.NodeBuilders; import me.lucko.luckperms.common.node.matcher.ConstraintNodeMatcher; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.StorageMetadata; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.misc.NodeEntry; import me.lucko.luckperms.common.storage.misc.PlayerSaveResultImpl; import me.lucko.luckperms.common.storage.misc.StorageCredentials; +import me.lucko.luckperms.common.util.Difference; +import me.lucko.luckperms.common.util.HostAndPort; import me.lucko.luckperms.common.util.Iterators; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import net.luckperms.api.actionlog.Action; import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextSet; @@ -66,15 +74,18 @@ import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.Node; import net.luckperms.api.node.NodeBuilder; - import org.bson.Document; +import org.bson.UuidRepresentation; +import org.bson.conversions.Bson; +import org.checkerframework.checker.nullness.qual.Nullable; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -109,8 +120,16 @@ public String getImplementationName() { @Override public void init() { + MongoClientOptions.Builder options = MongoClientOptions.builder() + .uuidRepresentation(UuidRepresentation.JAVA_LEGACY); + if (!Strings.isNullOrEmpty(this.connectionUri)) { - this.mongoClient = new MongoClient(new MongoClientURI(this.connectionUri)); + MongoClientURI uri = new MongoClientURI(this.connectionUri, options); + this.mongoClient = new MongoClient(uri); + + String databaseName = uri.getDatabase() != null ? uri.getDatabase() : this.configuration.getDatabase(); + this.database = this.mongoClient.getDatabase(databaseName); + } else { MongoCredential credential = null; if (!Strings.isNullOrEmpty(this.configuration.getUsername())) { @@ -121,19 +140,28 @@ public void init() { ); } - String[] addressSplit = this.configuration.getAddress().split(":"); - String host = addressSplit[0]; - int port = addressSplit.length > 1 ? Integer.parseInt(addressSplit[1]) : 27017; + HostAndPort hostAndPort = new HostAndPort(this.configuration.getAddress()) + .requireBracketsForIPv6() + .withDefaultPort(27017); + + String host = hostAndPort.getHost(); + int port = hostAndPort.getPort(); ServerAddress address = new ServerAddress(host, port); if (credential == null) { - this.mongoClient = new MongoClient(address); + this.mongoClient = new MongoClient(address, options.build()); } else { - this.mongoClient = new MongoClient(address, credential, MongoClientOptions.builder().build()); + this.mongoClient = new MongoClient(address, credential, options.build()); } + + this.database = this.mongoClient.getDatabase(this.configuration.getDatabase()); + } + + try { + ensureIndexes(); + } catch (MongoException e) { + // ignore } - - this.database = this.mongoClient.getDatabase(this.configuration.getDatabase()); } @Override @@ -144,108 +172,48 @@ public void shutdown() { } @Override - public Map getMeta() { - Map meta = new LinkedHashMap<>(); - boolean success = true; + public StorageMetadata getMeta() { + StorageMetadata metadata = new StorageMetadata(); + boolean success = true; long start = System.currentTimeMillis(); + try { this.database.runCommand(new Document("ping", 1)); } catch (Exception e) { success = false; } - long duration = System.currentTimeMillis() - start; if (success) { - meta.put( - Component.translatable("luckperms.command.info.storage.meta.ping-key"), - Component.text(duration + "ms", NamedTextColor.GREEN) - ); + int duration = (int) (System.currentTimeMillis() - start); + metadata.ping(duration); } - meta.put( - Component.translatable("luckperms.command.info.storage.meta.connected-key"), - Message.formatBoolean(success) - ); - return meta; + metadata.connected(success); + return metadata; } @Override public void logAction(Action entry) { MongoCollection c = this.database.getCollection(this.prefix + "action"); - - Document doc = new Document() - .append("timestamp", entry.getTimestamp().getEpochSecond()) - .append("source", new Document() - .append("uniqueId", entry.getSource().getUniqueId()) - .append("name", entry.getSource().getName()) - ); - - Document target = new Document() - .append("type", entry.getTarget().getType().name()) - .append("name", entry.getTarget().getName()); - - if (entry.getTarget().getUniqueId().isPresent()) { - target.append("uniqueId", entry.getTarget().getUniqueId().get()); - } - - doc.append("target", target); - doc.append("description", entry.getDescription()); - - c.insertOne(doc); + c.insertOne(actionToDoc(entry)); } @Override - public Log getLog() { - Log.Builder log = Log.builder(); - MongoCollection c = this.database.getCollection(this.prefix + "action"); - try (MongoCursor cursor = c.find().iterator()) { - while (cursor.hasNext()) { - Document d = cursor.next(); - - if (d.containsKey("source")) { - // new format - Document source = d.get("source", Document.class); - Document target = d.get("target", Document.class); - - UUID targetUniqueId = null; - if (target.containsKey("uniqueId")) { - targetUniqueId = target.get("uniqueId", UUID.class); - } + public LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws Exception { + Bson filter = ActionFilterMongoBuilder.INSTANCE.make(filters); - LoggedAction e = LoggedAction.build() - .timestamp(Instant.ofEpochSecond(d.getLong("timestamp"))) - .source(source.get("uniqueId", UUID.class)) - .sourceName(source.getString("name")) - .targetType(LoggedAction.parseType(target.getString("type"))) - .target(targetUniqueId) - .targetName(target.getString("name")) - .description(d.getString("description")) - .build(); - - log.add(e); - } else { - // old format - UUID actedUuid = null; - if (d.containsKey("acted")) { - actedUuid = d.get("acted", UUID.class); - } + MongoCollection c = this.database.getCollection(this.prefix + "action"); + long count = c.countDocuments(filter); - LoggedAction e = LoggedAction.build() - .timestamp(Instant.ofEpochSecond(d.getLong("timestamp"))) - .source(d.get("actor", UUID.class)) - .sourceName(d.getString("actorName")) - .targetType(LoggedAction.parseTypeCharacter(d.getString("type").charAt(0))) - .target(actedUuid) - .targetName(d.getString("actedName")) - .description(d.getString("action")) - .build(); - - log.add(e); - } + List content = new ArrayList<>(); + try (MongoCursor cursor = ConstraintMongoBuilder.page(page, c.find(filter).sort(Sorts.descending("timestamp", "_id"))).iterator()) { + while (cursor.hasNext()) { + content.add(actionFromDoc(cursor.next())); } } - return log.build(); + + return LogPage.of(content, page, (int) count); } @Override @@ -258,7 +226,7 @@ public void applyBulkUpdate(BulkUpdate bulkUpdate) { UUID uuid = getDocumentId(d); Document results = processBulkUpdate(d, bulkUpdate, HolderType.USER); if (results != null) { - c.replaceOne(new Document("_id", uuid), results); + c.replaceOne(Filters.eq("_id", uuid), results); } } } @@ -272,7 +240,7 @@ public void applyBulkUpdate(BulkUpdate bulkUpdate) { String holder = d.getString("_id"); Document results = processBulkUpdate(d, bulkUpdate, HolderType.GROUP); if (results != null) { - c.replaceOne(new Document("_id", holder), results); + c.replaceOne(Filters.eq("_id", holder), results); } } } @@ -297,31 +265,55 @@ private Document processBulkUpdate(Document document, BulkUpdate bulkUpdate, Hol @Override public User loadUser(UUID uniqueId, String username) { - User user = this.plugin.getUserManager().getOrMake(uniqueId, username); MongoCollection c = this.database.getCollection(this.prefix + "users"); - try (MongoCursor cursor = c.find(new Document("_id", user.getUniqueId())).iterator()) { - if (cursor.hasNext()) { - // User exists, let's load. - Document d = cursor.next(); - String name = d.getString("name"); - user.getPrimaryGroup().setStoredValue(d.getString("primaryGroup")); - user.setUsername(name, true); + Document document; + try (MongoCursor cursor = c.find(Filters.eq("_id", uniqueId)).iterator()) { + document = cursor.hasNext() ? cursor.next() : null; + } - user.loadNodesFromStorage(nodesFromDoc(d)); - this.plugin.getUserManager().giveDefaultIfNeeded(user); + return createUser(uniqueId, username, document); + } + @Override + public Map loadUsers(Set uniqueIds) { + MongoCollection c = this.database.getCollection(this.prefix + "users"); - boolean updatedUsername = user.getUsername().isPresent() && (name == null || !user.getUsername().get().equalsIgnoreCase(name)); - if (updatedUsername | user.auditTemporaryNodes()) { - c.replaceOne(new Document("_id", user.getUniqueId()), userToDoc(user)); - } - } else { - if (this.plugin.getUserManager().isNonDefaultUser(user)) { - user.loadNodesFromStorage(Collections.emptyList()); - user.getPrimaryGroup().setStoredValue(null); - this.plugin.getUserManager().giveDefaultIfNeeded(user); - } + Map documents = new HashMap<>(); + try (MongoCursor cursor = c.find(Filters.in("_id", uniqueIds)).iterator()) { + while (cursor.hasNext()) { + Document document = cursor.next(); + documents.put(getDocumentId(document), document); + } + } + + Map users = new HashMap<>(); + for (UUID uniqueId : uniqueIds) { + users.put(uniqueId, createUser(uniqueId, null, documents.get(uniqueId))); + } + return users; + } + + private User createUser(UUID uniqueId, String username, @Nullable Document document) { + User user = this.plugin.getUserManager().getOrMake(uniqueId, username); + if (document != null) { + String name = document.getString("name"); + + user.getPrimaryGroup().setStoredValue(document.getString("primaryGroup")); + user.setUsername(name, true); + + user.loadNodesFromStorage(nodesFromDoc(document)); + this.plugin.getUserManager().giveDefaultIfNeeded(user); + + boolean updatedUsername = user.getUsername().isPresent() && (name == null || !user.getUsername().get().equalsIgnoreCase(name)); + if (updatedUsername | user.auditTemporaryNodes()) { + saveUser(user); + } + } else { + if (this.plugin.getUserManager().isNonDefaultUser(user)) { + user.loadNodesFromStorage(Collections.emptyList()); + user.getPrimaryGroup().setStoredValue(null); + this.plugin.getUserManager().giveDefaultIfNeeded(user); } } return user; @@ -330,11 +322,40 @@ public User loadUser(UUID uniqueId, String username) { @Override public void saveUser(User user) { MongoCollection c = this.database.getCollection(this.prefix + "users"); - user.normalData().discardChanges(); - if (!this.plugin.getUserManager().isNonDefaultUser(user)) { - c.deleteOne(new Document("_id", user.getUniqueId())); - } else { - c.replaceOne(new Document("_id", user.getUniqueId()), userToDoc(user), new ReplaceOptions().upsert(true)); + + Difference changes = user.normalData().exportChanges(results -> { + if (this.plugin.getUserManager().isNonDefaultUser(user)) { + return true; + } + + // if the only change is adding the default node, we don't need to export + if (results.getChanges().size() == 1) { + Difference.Change onlyChange = results.getChanges().iterator().next(); + return !(onlyChange.type() == Difference.ChangeType.ADD && this.plugin.getUserManager().isDefaultNode(onlyChange.value())); + } + + 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) { + c.deleteOne(Filters.eq("_id", user.getUniqueId())); + return; + } + + List updates = nodeChangesToUpdates(changes); + updates.add(Updates.combine( + Updates.set("name", user.getUsername().orElse("null")), + Updates.set("primaryGroup", user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME)) + )); + for (Bson update : updates) { + c.updateOne(Filters.eq("_id", user.getUniqueId()), update, new UpdateOptions().upsert(true)); } } @@ -358,7 +379,7 @@ public Set getUniqueUsers() { public List> searchUserNodes(ConstraintNodeMatcher constraint) throws Exception { List> held = new ArrayList<>(); MongoCollection c = this.database.getCollection(this.prefix + "users"); - try (MongoCursor cursor = c.find().iterator()) { + try (MongoCursor cursor = c.find(Filters.elemMatch("permissions", ConstraintMongoBuilder.INSTANCE.make(constraint.getConstraint(), "key"))).iterator()) { while (cursor.hasNext()) { Document d = cursor.next(); UUID holder = getDocumentId(d); @@ -379,12 +400,12 @@ public List> searchUserNodes(ConstraintNodeM public Group createAndLoadGroup(String name) { Group group = this.plugin.getGroupManager().getOrMake(name); MongoCollection c = this.database.getCollection(this.prefix + "groups"); - try (MongoCursor cursor = c.find(new Document("_id", group.getName())).iterator()) { - if (cursor.hasNext()) { + try (MongoCursor cursor = c.find(Filters.eq("_id", group.getName())).iterator()) { + if (!cursor.hasNext()) { + c.insertOne(new Document("_id", group.getName())); + } else { Document d = cursor.next(); group.loadNodesFromStorage(nodesFromDoc(d)); - } else { - c.insertOne(groupToDoc(group)); } } return group; @@ -393,7 +414,7 @@ public Group createAndLoadGroup(String name) { @Override public Optional loadGroup(String name) { MongoCollection c = this.database.getCollection(this.prefix + "groups"); - try (MongoCursor cursor = c.find(new Document("_id", name)).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("_id", name)).iterator()) { if (!cursor.hasNext()) { return Optional.empty(); } @@ -426,21 +447,25 @@ public void loadAllGroups() { @Override public void saveGroup(Group group) { MongoCollection c = this.database.getCollection(this.prefix + "groups"); - group.normalData().discardChanges(); - c.replaceOne(new Document("_id", group.getName()), groupToDoc(group), new ReplaceOptions().upsert(true)); + Difference changes = group.normalData().exportChanges(results -> true); + + List updates = nodeChangesToUpdates(changes); + for (Bson update : updates) { + c.updateOne(Filters.eq("_id", group.getName()), update, new UpdateOptions().upsert(true)); + } } @Override public void deleteGroup(Group group) { MongoCollection c = this.database.getCollection(this.prefix + "groups"); - c.deleteOne(new Document("_id", group.getName())); + c.deleteOne(Filters.eq("_id", group.getName())); } @Override public List> searchGroupNodes(ConstraintNodeMatcher constraint) throws Exception { List> held = new ArrayList<>(); MongoCollection c = this.database.getCollection(this.prefix + "groups"); - try (MongoCursor cursor = c.find().iterator()) { + try (MongoCursor cursor = c.find(Filters.elemMatch("permissions", ConstraintMongoBuilder.INSTANCE.make(constraint.getConstraint(), "key"))).iterator()) { while (cursor.hasNext()) { Document d = cursor.next(); String holder = d.getString("_id"); @@ -461,7 +486,7 @@ public List> searchGroupNodes(ConstraintNo public Track createAndLoadTrack(String name) { Track track = this.plugin.getTrackManager().getOrMake(name); MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - try (MongoCursor cursor = c.find(new Document("_id", track.getName())).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("_id", track.getName())).iterator()) { if (!cursor.hasNext()) { c.insertOne(trackToDoc(track)); } else { @@ -476,7 +501,7 @@ public Track createAndLoadTrack(String name) { @Override public Optional loadTrack(String name) { MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - try (MongoCursor cursor = c.find(new Document("_id", name)).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("_id", name)).iterator()) { if (!cursor.hasNext()) { return Optional.empty(); } @@ -510,18 +535,18 @@ public void loadAllTracks() { @Override public void saveTrack(Track track) { MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - c.replaceOne(new Document("_id", track.getName()), trackToDoc(track)); + c.replaceOne(Filters.eq("_id", track.getName()), trackToDoc(track)); } @Override public void deleteTrack(Track track) { MongoCollection c = this.database.getCollection(this.prefix + "tracks"); - c.deleteOne(new Document("_id", track.getName())); + c.deleteOne(Filters.eq("_id", track.getName())); } @Override public PlayerSaveResult savePlayerData(UUID uniqueId, String username) { - username = username.toLowerCase(); + username = username.toLowerCase(Locale.ROOT); MongoCollection c = this.database.getCollection(this.prefix + "uuid"); // find any existing mapping @@ -529,13 +554,13 @@ public PlayerSaveResult savePlayerData(UUID uniqueId, String username) { // do the insert if (!username.equalsIgnoreCase(oldUsername)) { - c.replaceOne(new Document("_id", uniqueId), new Document("_id", uniqueId).append("name", username), new ReplaceOptions().upsert(true)); + c.replaceOne(Filters.eq("_id", uniqueId), new Document("_id", uniqueId).append("name", username), new ReplaceOptions().upsert(true)); } PlayerSaveResultImpl result = PlayerSaveResultImpl.determineBaseResult(username, oldUsername); Set conflicting = new HashSet<>(); - try (MongoCursor cursor = c.find(new Document("name", username)).iterator()) { + try (MongoCursor cursor = c.find(Filters.eq("name", username)).iterator()) { while (cursor.hasNext()) { conflicting.add(getDocumentId(cursor.next())); } @@ -544,7 +569,7 @@ public PlayerSaveResult savePlayerData(UUID uniqueId, String username) { if (!conflicting.isEmpty()) { // remove the mappings for conflicting uuids - c.deleteMany(Filters.and(conflicting.stream().map(u -> Filters.eq("_id", u)).collect(Collectors.toList()))); + c.deleteMany(Filters.or(conflicting.stream().map(u -> Filters.eq("_id", u)).collect(Collectors.toList()))); result = result.withOtherUuidsPresent(conflicting); } @@ -560,7 +585,7 @@ public void deletePlayerData(UUID uniqueId) { @Override public UUID getPlayerUniqueId(String username) { MongoCollection c = this.database.getCollection(this.prefix + "uuid"); - Document doc = c.find(new Document("name", username.toLowerCase())).first(); + Document doc = c.find(Filters.eq("name", username.toLowerCase(Locale.ROOT))).first(); if (doc != null) { return getDocumentId(doc); } @@ -570,9 +595,12 @@ public UUID getPlayerUniqueId(String username) { @Override public String getPlayerName(UUID uniqueId) { MongoCollection c = this.database.getCollection(this.prefix + "uuid"); - Document doc = c.find(new Document("_id", uniqueId)).first(); + Document doc = c.find(Filters.eq("_id", uniqueId)).first(); if (doc != null) { - return doc.get("name", String.class); + String username = doc.get("name", String.class); + if (username != null && !username.equals("null")) { + return username; + } } return null; } @@ -588,17 +616,6 @@ private static UUID getDocumentId(Document document) { } } - private static Document userToDoc(User user) { - List nodes = user.normalData().asList().stream() - .map(MongoStorage::nodeToDoc) - .collect(Collectors.toList()); - - return new Document("_id", user.getUniqueId()) - .append("name", user.getUsername().orElse("null")) - .append("primaryGroup", user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME)) - .append("permissions", nodes); - } - private static List nodesFromDoc(Document document) { List nodes = new ArrayList<>(); if (document.containsKey("permissions") && document.get("permissions") instanceof List) { @@ -614,23 +631,34 @@ private static List nodesFromDoc(Document document) { return nodes; } - private static Document groupToDoc(Group group) { - List nodes = group.normalData().asList().stream() - .map(MongoStorage::nodeToDoc) - .collect(Collectors.toList()); - - return new Document("_id", group.getName()).append("permissions", nodes); - } - private static Document trackToDoc(Track track) { return new Document("_id", track.getName()).append("groups", track.getGroups()); } - private static Document nodeToDoc(Node node) { - Document document = new Document(); + private static List nodeChangesToUpdates(Difference changes) { + List updates = new ArrayList<>(); + + Set removed = changes.getRemoved(); + Set added = changes.getAdded(); + + if (!removed.isEmpty()) { + List docs = removed.stream().map(MongoStorage::nodeToDoc).collect(Collectors.toList()); + updates.add(Updates.pullAll("permissions", docs)); + } + + if (!added.isEmpty()) { + List docs = added.stream().map(MongoStorage::nodeToDoc).collect(Collectors.toList()); + updates.add(Updates.pushEach("permissions", docs)); + } + + return updates; + } - document.append("key", node.getKey()); - document.append("value", node.getValue()); + @VisibleForTesting + static Document nodeToDoc(Node node) { + Document document = new Document() + .append("key", node.getKey()) + .append("value", node.getValue()); Instant expiry = node.getExpiry(); if (expiry != null) { @@ -644,7 +672,8 @@ private static Document nodeToDoc(Node node) { return document; } - private static Node nodeFromDoc(Document document) { + @VisibleForTesting + static Node nodeFromDoc(Document document) { String key = document.containsKey("permission") ? document.getString("permission") : document.getString("key"); if (key == null || key.isEmpty()) { @@ -663,7 +692,7 @@ private static Node nodeFromDoc(Document document) { } if (document.containsKey("expiry")) { - builder.expiry(document.getLong("expiry")); + builder.expiry(((Number) document.get("expiry")).longValue()); } if (document.containsKey("context") && document.get("context") instanceof List) { @@ -691,4 +720,68 @@ private static MutableContextSet docsToContextSet(List documents) { return map; } + private static Document actionToDoc(Action action) { + Document source = new Document() + .append("uniqueId", action.getSource().getUniqueId()) + .append("name", action.getSource().getName()); + + Document target = new Document() + .append("type", action.getTarget().getType().name()) + .append("name", action.getTarget().getName()); + if (action.getTarget().getUniqueId().isPresent()) { + target.append("uniqueId", action.getTarget().getUniqueId().get()); + } + + return new Document() + .append("timestamp", action.getTimestamp().getEpochSecond()) + .append("source", source) + .append("target", target) + .append("description", action.getDescription()); + } + + private static LoggedAction actionFromDoc(Document d) { + if (d.containsKey("source")) { + // new format + Document source = d.get("source", Document.class); + Document target = d.get("target", Document.class); + + UUID targetUniqueId = null; + if (target.containsKey("uniqueId")) { + targetUniqueId = target.get("uniqueId", UUID.class); + } + + return LoggedAction.build() + .timestamp(Instant.ofEpochSecond(d.getLong("timestamp"))) + .source(source.get("uniqueId", UUID.class)) + .sourceName(source.getString("name")) + .targetType(LoggedAction.parseType(target.getString("type"))) + .target(targetUniqueId) + .targetName(target.getString("name")) + .description(d.getString("description")) + .build(); + } else { + // old format + UUID actedUuid = null; + if (d.containsKey("acted")) { + actedUuid = d.get("acted", UUID.class); + } + + return LoggedAction.build() + .timestamp(Instant.ofEpochSecond(d.getLong("timestamp"))) + .source(d.get("actor", UUID.class)) + .sourceName(d.getString("actorName")) + .targetType(LoggedAction.parseTypeCharacter(d.getString("type").charAt(0))) + .target(actedUuid) + .targetName(d.getString("actedName")) + .description(d.getString("action")) + .build(); + } + } + + private void ensureIndexes() { + this.database.getCollection(this.prefix + "uuid").createIndex(Indexes.ascending("name")); + this.database.getCollection(this.prefix + "users").createIndex(Indexes.ascending("permissions.key")); + this.database.getCollection(this.prefix + "groups").createIndex(Indexes.ascending("permissions.key")); + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/rest/RestStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/rest/RestStorage.java new file mode 100644 index 000000000..a94c3e1b9 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/rest/RestStorage.java @@ -0,0 +1,664 @@ +/* + * 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.storage.implementation.rest; + +import com.google.common.collect.ImmutableList; +import me.lucko.luckperms.common.actionlog.LogPage; +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFields; +import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.Constraint; +import me.lucko.luckperms.common.filter.Filter; +import me.lucko.luckperms.common.filter.FilterField; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; +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.factory.NodeBuilders; +import me.lucko.luckperms.common.node.matcher.ConstraintNodeMatcher; +import me.lucko.luckperms.common.node.matcher.StandardNodeMatchers; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.StorageMetadata; +import me.lucko.luckperms.common.storage.implementation.StorageImplementation; +import me.lucko.luckperms.common.storage.misc.NodeEntry; +import me.lucko.luckperms.common.storage.misc.PlayerSaveResultImpl; +import me.lucko.luckperms.common.util.Difference; +import me.lucko.luckperms.common.util.Iterators; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.model.PlayerSaveResult; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeBuilder; +import net.luckperms.api.node.NodeType; +import net.luckperms.rest.LuckPermsRestClient; +import net.luckperms.rest.model.ActionPage; +import net.luckperms.rest.model.Context; +import net.luckperms.rest.model.CreateGroupRequest; +import net.luckperms.rest.model.CreateTrackRequest; +import net.luckperms.rest.model.CreateUserRequest; +import net.luckperms.rest.model.GroupSearchResult; +import net.luckperms.rest.model.Health; +import net.luckperms.rest.model.UpdateTrackRequest; +import net.luckperms.rest.model.UpdateUserRequest; +import net.luckperms.rest.model.UserLookupResult; +import net.luckperms.rest.model.UserSearchResult; +import org.checkerframework.checker.nullness.qual.Nullable; +import retrofit2.Response; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class RestStorage implements StorageImplementation { + private final LuckPermsPlugin plugin; + private final LuckPermsRestClient client; + + public RestStorage(LuckPermsPlugin plugin, String baseUrl, String apiKey) { + this.plugin = plugin; + this.client = LuckPermsRestClient.builder() + .baseUrl(baseUrl) + .apiKey(apiKey) + .build(); + } + + @Override + public LuckPermsPlugin getPlugin() { + return this.plugin; + } + + @Override + public String getImplementationName() { + return "REST"; + } + + @Override + public void init() throws IOException { + Health health = this.client.misc().health().execute().body(); + if (health == null || !health.healthy()) { + this.plugin.getLogger().warn("REST storage endpoint is unhealthy"); + } + } + + @Override + public void shutdown() { + this.client.close(); + } + + @Override + public StorageMetadata getMeta() { + StorageMetadata metadata = new StorageMetadata(); + + boolean success = true; + long start = System.currentTimeMillis(); + try { + Health health = this.client.misc().health().execute().body(); + if (health == null || !health.healthy()) { + success = false; + } + } catch (IOException e) { + success = false; + } + + if (success) { + int duration = (int) (System.currentTimeMillis() - start); + metadata.ping(duration); + } + + metadata.connected(success); + return metadata; + } + + @Override + public void logAction(Action entry) throws IOException { + this.client.actions().submit(convertAction(entry)).execute(); + } + + @Override + public LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws Exception { + Response resp = null; + + if (filters.isEmpty()) { + // ActionFilters.all() + resp = page != null + ? this.client.actions().query(page.pageSize(), page.pageNumber()).execute() + : this.client.actions().query().execute(); + + } else if (filters.size() == 1) { + // ActionFilters.source(uniqueId) + Filter filter = filters.get(0); + if (filter.field() == ActionFields.SOURCE_UNIQUE_ID && filter.constraint().comparison() == Comparison.EQUAL) { + UUID uniqueId = (UUID) filter.constraint().value(); + resp = page != null + ? this.client.actions().querySource(uniqueId, page.pageSize(), page.pageNumber()).execute() + : this.client.actions().querySource(uniqueId).execute(); + } + + } else if (filters.operator() == FilterList.LogicalOperator.AND && filters.size() == 2) { + Filter filterA = filters.get(0); + Filter filterB = filters.get(1); + + if (filterA.field() == ActionFields.TARGET_TYPE && filterA.constraint().comparison() == Comparison.EQUAL) { + Action.Target.Type type = (Action.Target.Type) filterA.constraint().value(); + + if (type == Action.Target.Type.USER) { + // ActionFilters.user(uniqueId) + if (filterB.field() == ActionFields.TARGET_UNIQUE_ID && filterB.constraint().comparison() == Comparison.EQUAL) { + UUID uniqueId = (UUID) filterB.constraint().value(); + resp = page != null + ? this.client.actions().queryTargetUser(uniqueId, page.pageSize(), page.pageNumber()).execute() + : this.client.actions().queryTargetUser(uniqueId).execute(); + } + } else if (type == Action.Target.Type.GROUP) { + // ActionFilters.group(name) + if (filterB.field() == ActionFields.TARGET_NAME && filterB.constraint().comparison() == Comparison.EQUAL) { + String name = (String) filterB.constraint().value(); + resp = page != null + ? this.client.actions().queryTargetGroup(name, page.pageSize(), page.pageNumber()).execute() + : this.client.actions().queryTargetGroup(name).execute(); + } + } else if (type == Action.Target.Type.TRACK) { + // ActionFilters.track(name) + if (filterB.field() == ActionFields.TARGET_NAME && filterB.constraint().comparison() == Comparison.EQUAL) { + String name = (String) filterB.constraint().value(); + resp = page != null + ? this.client.actions().queryTargetTrack(name, page.pageSize(), page.pageNumber()).execute() + : this.client.actions().queryTargetTrack(name).execute(); + } + } + } + + } else if (filters.operator() == FilterList.LogicalOperator.OR && filters.size() == 3) { + // ActionFilters.search(query) + ImmutableList> searchFields = ImmutableList.of(ActionFields.SOURCE_NAME, ActionFields.TARGET_NAME, ActionFields.DESCRIPTION); + if (filters.stream().allMatch(filter -> filter.constraint().comparison() == Comparison.SIMILAR && searchFields.contains(filter.field()))) { + String query = (String) filters.get(0).constraint().value(); + if (!query.startsWith("%") || !query.endsWith("%")) { + throw new IllegalArgumentException("Unsupported search query: " + query); + } + String searchTerm = query.substring(1, query.length() - 1); + + resp = page != null + ? this.client.actions().querySearch(searchTerm, page.pageSize(), page.pageNumber()).execute() + : this.client.actions().querySearch(searchTerm).execute(); + } + } + + if (resp == null) { + throw new UnsupportedOperationException("Unsupported filter: " + filters); + } + + ActionPage body = Objects.requireNonNull(resp.body(), "resp.body()"); + return LogPage.of( + body.entries().stream().map(RestStorage::convertAction).collect(Collectors.toList()), + page, + body.overallSize() + ); + } + + @Override + public void applyBulkUpdate(BulkUpdate bulkUpdate) { + throw new UnsupportedOperationException(); // TODO + } + + @Override + public User loadUser(UUID uniqueId, String username) throws Exception { + net.luckperms.rest.model.User remoteUser = this.client.users().get(uniqueId).execute().body(); + if (remoteUser == null) { + throw new IllegalStateException("Client did not return a user for " + uniqueId); + } + + User user = this.plugin.getUserManager().getOrMake(uniqueId, username); + user.setUsername(remoteUser.username(), true); + user.loadNodesFromStorage(remoteUser.nodes().stream().map(RestStorage::convertNode).collect(Collectors.toList())); + + return user; + } + + @Override + public Map loadUsers(Set uniqueIds) throws Exception { + return uniqueIds.parallelStream() + .map(uniqueId -> { + try { + return loadUser(uniqueId, null); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toMap(User::getUniqueId, Function.identity())); + } + + @Override + public void saveUser(User user) throws Exception { + Difference changes = user.normalData().exportChanges(results -> { + if (this.plugin.getUserManager().isNonDefaultUser(user)) { + return true; + } + + // if the only change is adding the default node, we don't need to export + if (results.getChanges().size() == 1) { + Difference.Change onlyChange = results.getChanges().iterator().next(); + return !(onlyChange.type() == Difference.ChangeType.ADD && this.plugin.getUserManager().isDefaultNode(onlyChange.value())); + } + + return true; + }); + if (changes == null) { + return; + } + + String username = user.getUsername().orElse(null); + if (username != null) { + this.client.users().update(user.getUniqueId(), new UpdateUserRequest(username)).execute(); + } + + Set added = changes.getAdded(); + Set removed = changes.getRemoved(); + + if (!removed.isEmpty()) { + this.client.users().nodesDelete(user.getUniqueId(), removed.stream().map(RestStorage::convertNode).collect(Collectors.toList())).execute(); + } + if (!added.isEmpty()) { + this.client.users().nodesAdd(user.getUniqueId(), added.stream().map(RestStorage::convertNode).collect(Collectors.toList())).execute(); + } + } + + @Override + public Set getUniqueUsers() throws Exception { + return this.client.users().list().execute().body(); + } + + @Override + public List> searchUserNodes(ConstraintNodeMatcher matcher) throws Exception { + List results; + if (matcher instanceof StandardNodeMatchers.TypeEquals) { + NodeType type = ((StandardNodeMatchers.TypeEquals) matcher).getType(); + results = this.client.users().searchNodesByType(convertNodeType(type)).execute().body(); + } else { + Constraint constraint = matcher.getConstraint(); + Comparison comparison = constraint.comparison(); + String value = constraint.value(); + + if (comparison == Comparison.EQUAL) { + results = this.client.users().searchNodesByKey(value).execute().body(); + } else if (comparison == Comparison.SIMILAR) { + long wildcards = value.chars().filter(ch -> ch == '%').count(); + if (wildcards == 0) { + results = this.client.users().searchNodesByKey(value).execute().body(); + } else if (wildcards == 1 && value.endsWith("%")) { + results = this.client.users().searchNodesByKeyStartsWith(value.substring(0, value.length() - 1)).execute().body(); + } else { + throw new UnsupportedOperationException("Unsupported constraint: " + constraint); + } + } else { + throw new UnsupportedOperationException("Unsupported constraint: " + constraint); + } + } + + if (results == null) { + throw new IllegalStateException("Client returned null results"); + } + + List> held = new ArrayList<>(); + for (UserSearchResult result : results) { + UUID uniqueId = result.uniqueId(); + for (net.luckperms.rest.model.Node node : result.results()) { + N match = matcher.filterConstraintMatch(convertNode(node)); + if (match != null) { + held.add(NodeEntry.of(uniqueId, match)); + } + } + } + return held; + } + + @Override + public Group createAndLoadGroup(String name) throws Exception { + net.luckperms.rest.model.Group remoteGroup = this.client.groups().create(new CreateGroupRequest(name)).execute().body(); + if (remoteGroup == null) { + remoteGroup = this.client.groups().get(name).execute().body(); + if (remoteGroup == null) { + throw new IllegalStateException("Unable to create group: " + name); + } + } + + Group group = this.plugin.getGroupManager().getOrMake(name); + group.loadNodesFromStorage(remoteGroup.nodes().stream().map(RestStorage::convertNode).collect(Collectors.toList())); + return group; + } + + @Override + public Optional loadGroup(String name) throws Exception { + net.luckperms.rest.model.Group remoteGroup = this.client.groups().get(name).execute().body(); + if (remoteGroup == null) { + return Optional.empty(); + } + + Group group = this.plugin.getGroupManager().getOrMake(name); + group.loadNodesFromStorage(remoteGroup.nodes().stream().map(RestStorage::convertNode).collect(Collectors.toList())); + return Optional.of(group); + } + + @Override + public void loadAllGroups() throws Exception { + Set groups = this.client.groups().list().execute().body(); + if (groups == null) { + throw new IllegalStateException("Client returned a null list of groups"); + } + + if (!Iterators.tryIterate(groups, this::loadGroup)) { + throw new RuntimeException("Exception occurred whilst loading a group"); + } + + this.plugin.getGroupManager().retainAll(groups); + } + + @Override + public void saveGroup(Group group) throws Exception { + Difference changes = group.normalData().exportChanges(c -> true); + + Set added = changes.getAdded(); + Set removed = changes.getRemoved(); + + if (!removed.isEmpty()) { + this.client.groups().nodesDelete(group.getName(), removed.stream().map(RestStorage::convertNode).collect(Collectors.toList())).execute(); + } + if (!added.isEmpty()) { + this.client.groups().nodesAdd(group.getName(), added.stream().map(RestStorage::convertNode).collect(Collectors.toList())).execute(); + } + } + + @Override + public void deleteGroup(Group group) throws Exception { + this.client.groups().delete(group.getName()).execute(); + } + + @Override + public List> searchGroupNodes(ConstraintNodeMatcher matcher) throws Exception { + List results; + if (matcher instanceof StandardNodeMatchers.TypeEquals) { + NodeType type = ((StandardNodeMatchers.TypeEquals) matcher).getType(); + results = this.client.groups().searchNodesByType(convertNodeType(type)).execute().body(); + } else { + Constraint constraint = matcher.getConstraint(); + Comparison comparison = constraint.comparison(); + String value = constraint.value(); + + if (comparison == Comparison.EQUAL) { + results = this.client.groups().searchNodesByKey(value).execute().body(); + } else if (comparison == Comparison.SIMILAR) { + long wildcards = value.chars().filter(ch -> ch == '%').count(); + if (wildcards == 0) { + results = this.client.groups().searchNodesByKey(value).execute().body(); + } else if (wildcards == 1 && value.endsWith("%")) { + results = this.client.groups().searchNodesByKeyStartsWith(value.substring(0, value.length() - 1)).execute().body(); + } else { + throw new UnsupportedOperationException("Unsupported constraint: " + constraint); + } + } else { + throw new UnsupportedOperationException("Unsupported constraint: " + constraint); + } + } + + if (results == null) { + throw new IllegalStateException("Client returned null results"); + } + + List> held = new ArrayList<>(); + for (GroupSearchResult result : results) { + String name = result.name(); + for (net.luckperms.rest.model.Node node : result.results()) { + N match = matcher.filterConstraintMatch(convertNode(node)); + if (match != null) { + held.add(NodeEntry.of(name, match)); + } + } + } + return held; + } + + @Override + public Track createAndLoadTrack(String name) throws Exception { + net.luckperms.rest.model.Track remoteTrack = this.client.tracks().create(new CreateTrackRequest(name)).execute().body(); + if (remoteTrack == null) { + remoteTrack = this.client.tracks().get(name).execute().body(); + if (remoteTrack == null) { + throw new IllegalStateException("Unable to create track: " + name); + } + } + + Track track = this.plugin.getTrackManager().getOrMake(name); + track.setGroups(remoteTrack.groups()); + return track; + } + + @Override + public Optional loadTrack(String name) throws Exception { + net.luckperms.rest.model.Track remoteTrack = this.client.tracks().get(name).execute().body(); + if (remoteTrack == null) { + return Optional.empty(); + } + + Track track = this.plugin.getTrackManager().getOrMake(name); + track.setGroups(remoteTrack.groups()); + return Optional.of(track); + } + + @Override + public void loadAllTracks() throws Exception { + Set tracks = this.client.tracks().list().execute().body(); + if (tracks == null) { + throw new IllegalStateException("Client returned a null list of tracks"); + } + + if (!Iterators.tryIterate(tracks, this::loadTrack)) { + throw new RuntimeException("Exception occurred whilst loading a track"); + } + + this.plugin.getTrackManager().retainAll(tracks); + } + + @Override + public void saveTrack(Track track) throws Exception { + this.client.tracks().update(track.getName(), new UpdateTrackRequest(track.getGroups())).execute(); + } + + @Override + public void deleteTrack(Track track) throws Exception { + this.client.tracks().delete(track.getName()).execute(); + } + + @Override + public PlayerSaveResult savePlayerData(UUID uniqueId, String username) throws Exception { + net.luckperms.rest.model.PlayerSaveResult remoteResult = this.client.users().create(new CreateUserRequest(uniqueId, username)).execute().body(); + + Set outcomes = remoteResult.outcomes().stream().map(outcome -> { + switch (outcome) { + case CLEAN_INSERT: + return PlayerSaveResult.Outcome.CLEAN_INSERT; + case NO_CHANGE: + return PlayerSaveResult.Outcome.NO_CHANGE; + case USERNAME_UPDATED: + return PlayerSaveResult.Outcome.USERNAME_UPDATED; + case OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME: + return PlayerSaveResult.Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME; + default: + throw new AssertionError(outcome); + } + }).collect(Collectors.toSet()); + + if (outcomes.isEmpty()) { + throw new IllegalStateException("No outcomes returned"); + } + + PlayerSaveResultImpl result; + if (outcomes.contains(PlayerSaveResult.Outcome.CLEAN_INSERT)) { + result = PlayerSaveResultImpl.cleanInsert(); + } else if (outcomes.contains(PlayerSaveResult.Outcome.NO_CHANGE)) { + result = PlayerSaveResultImpl.noChange(); + } else if (outcomes.contains(PlayerSaveResult.Outcome.USERNAME_UPDATED)) { + result = PlayerSaveResultImpl.usernameUpdated(remoteResult.previousUsername()); + } else { + throw new IllegalStateException("No base outcome returned"); + } + + if (outcomes.contains(PlayerSaveResult.Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME)) { + result = result.withOtherUuidsPresent(remoteResult.otherUniqueIds()); + } + + return result; + } + + @Override + public void deletePlayerData(UUID uniqueId) throws Exception { + this.client.users().delete(uniqueId, true).execute(); + } + + @Override + public @Nullable UUID getPlayerUniqueId(String username) throws Exception { + UserLookupResult result = this.client.users().lookup(username).execute().body(); + return result == null ? null : result.uniqueId(); + } + + @Override + public @Nullable String getPlayerName(UUID uniqueId) throws Exception { + UserLookupResult result = this.client.users().lookup(uniqueId).execute().body(); + return result == null ? null : result.username(); + } + + private static net.luckperms.rest.model.Action convertAction(Action action) { + return new net.luckperms.rest.model.Action( + action.getTimestamp().getEpochSecond(), + new net.luckperms.rest.model.Action.Source( + action.getSource().getUniqueId(), + action.getSource().getName() + ), + new net.luckperms.rest.model.Action.Target( + action.getTarget().getUniqueId().orElse(null), + action.getTarget().getName(), + convertActionTargetType(action.getTarget().getType()) + ), + action.getDescription() + ); + } + + private static net.luckperms.rest.model.Action.Target.Type convertActionTargetType(Action.Target.Type type) { + switch (type) { + case USER: + return net.luckperms.rest.model.Action.Target.Type.USER; + case GROUP: + return net.luckperms.rest.model.Action.Target.Type.GROUP; + case TRACK: + return net.luckperms.rest.model.Action.Target.Type.TRACK; + default: + throw new AssertionError(type); + } + } + + private static LoggedAction convertAction(net.luckperms.rest.model.Action action) { + return LoggedAction.build() + .timestamp(Instant.ofEpochSecond(action.timestamp())) + .source(action.source().uniqueId()) + .sourceName(action.source().name()) + .target(action.target().uniqueId()) + .targetName(action.target().name()) + .targetType(convertActionTargetType(action.target().type())) + .description(action.description()) + .build(); + } + + private static Action.Target.Type convertActionTargetType(net.luckperms.rest.model.Action.Target.Type type) { + switch (type) { + case USER: + return Action.Target.Type.USER; + case GROUP: + return Action.Target.Type.GROUP; + case TRACK: + return Action.Target.Type.TRACK; + default: + throw new AssertionError(type); + } + } + + private static Node convertNode(net.luckperms.rest.model.Node node) { + NodeBuilder builder = NodeBuilders.determineMostApplicable(node.key()); + if (node.value() != null) { + builder.value(node.value()); + } + if (node.context() != null) { + builder.context(convertContexts(node.context())); + } + if (node.expiry() != null) { + builder.expiry(node.expiry()); + } + return builder.build(); + } + + private static net.luckperms.rest.model.Node convertNode(Node node) { + return new net.luckperms.rest.model.Node( + node.getKey(), + node.getValue(), + convertContexts(node.getContexts()), + node.getExpiry() == null ? null : node.getExpiry().getEpochSecond() + ); + } + + public static ContextSet convertContexts(Set contexts) { + ImmutableContextSetImpl.BuilderImpl builder = new ImmutableContextSetImpl.BuilderImpl(); + for (Context context : contexts) { + builder.add(context.key(), context.value()); + } + return builder.build(); + } + + public static Set convertContexts(ContextSet contexts) { + return StreamSupport.stream(contexts.spliterator(), false) + .map(c -> new Context(c.getKey(), c.getValue())) + .collect(Collectors.toSet()); + } + + public static net.luckperms.rest.model.NodeType convertNodeType(NodeType type) { + if (type == NodeType.REGEX_PERMISSION) return net.luckperms.rest.model.NodeType.REGEX_PERMISSION; + if (type == NodeType.INHERITANCE) return net.luckperms.rest.model.NodeType.INHERITANCE; + if (type == NodeType.PREFIX) return net.luckperms.rest.model.NodeType.PREFIX; + if (type == NodeType.SUFFIX) return net.luckperms.rest.model.NodeType.SUFFIX; + if (type == NodeType.META) return net.luckperms.rest.model.NodeType.META; + if (type == NodeType.WEIGHT) return net.luckperms.rest.model.NodeType.WEIGHT; + if (type == NodeType.DISPLAY_NAME) return net.luckperms.rest.model.NodeType.DISPLAY_NAME; + throw new IllegalArgumentException("Invalid type: " + type.name()); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java index 06aa16294..e0a1c8397 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/split/SplitStorage.java @@ -26,31 +26,29 @@ package me.lucko.luckperms.common.storage.implementation.split; import com.google.common.collect.ImmutableMap; - -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; -import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; 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.ConstraintNodeMatcher; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.StorageMetadata; import me.lucko.luckperms.common.storage.StorageType; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.misc.NodeEntry; - -import net.kyori.adventure.text.Component; import net.luckperms.api.actionlog.Action; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.Node; +import org.checkerframework.checker.nullness.qual.Nullable; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; public class SplitStorage implements StorageImplementation { private final LuckPermsPlugin plugin; @@ -109,18 +107,12 @@ public void shutdown() { } @Override - public Map getMeta() { - Map meta = new LinkedHashMap<>(); - meta.put( - Component.translatable("luckperms.command.info.storage.meta.split-types-key"), - Message.formatStringList(this.types.entrySet().stream() - .map(e -> e.getKey().toString().toLowerCase() + "->" + e.getValue().getName().toLowerCase()) - .collect(Collectors.toList())) - ); + public StorageMetadata getMeta() { + StorageMetadata metadata = new StorageMetadata(); for (StorageImplementation backing : this.implementations.values()) { - meta.putAll(backing.getMeta()); + metadata.combine(backing.getMeta()); } - return meta; + return metadata; } @Override @@ -129,8 +121,8 @@ public void logAction(Action entry) throws Exception { } @Override - public Log getLog() throws Exception { - return implFor(SplitStorageType.LOG).getLog(); + public LogPage getLogPage(FilterList filters, @Nullable PageParameters page) throws Exception { + return implFor(SplitStorageType.LOG).getLogPage(filters, page); } @Override @@ -151,6 +143,11 @@ public User loadUser(UUID uniqueId, String username) throws Exception { return implFor(SplitStorageType.USER).loadUser(uniqueId, username); } + @Override + public Map loadUsers(Set uniqueIds) throws Exception { + return implFor(SplitStorageType.USER).loadUsers(uniqueIds); + } + @Override public void saveUser(User user) throws Exception { implFor(SplitStorageType.USER).saveUser(user); diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReader.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReader.java index bb7e48b00..c40fc6e17 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReader.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReader.java @@ -32,10 +32,24 @@ import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; public final class SchemaReader { private SchemaReader() {} + private static final Pattern CREATE_TABLE_PATTERN = Pattern.compile("^CREATE TABLE [`\"']([^`\"']+)[`\"'].*"); + private static final Pattern CREATE_INDEX_PATTERN = Pattern.compile("^CREATE INDEX.* ON [`\"']([^`\"']+)[`\"'].*"); + + /** + * Parses a schema file to a list of SQL statements + * + * @param is the input stream to read from + * @return a list of statements + * @throws IOException if an error occurs whilst reading the file + */ public static List getStatements(InputStream is) throws IOException { List queries = new LinkedList<>(); @@ -53,7 +67,7 @@ public static List getStatements(InputStream is) throws IOException { if (line.endsWith(";")) { sb.deleteCharAt(sb.length() - 1); - String result = sb.toString().trim(); + String result = sb.toString().trim().replaceAll(" +", " "); if (!result.isEmpty()) { queries.add(result); } @@ -66,4 +80,29 @@ public static List getStatements(InputStream is) throws IOException { return queries; } + + public static String tableFromStatement(String statement) { + Matcher table = CREATE_TABLE_PATTERN.matcher(statement); + if (table.matches()) { + return table.group(1).toLowerCase(Locale.ROOT); + } + Matcher index = CREATE_INDEX_PATTERN.matcher(statement); + if (index.matches()) { + return index.group(1).toLowerCase(Locale.ROOT); + } + throw new IllegalArgumentException("Unknown statement type: " + statement); + } + + /** + * Filters which statements should be executed based on the current list of tables in the database + * + * @param statements the statements to filter + * @param currentTables the current tables in the database + * @return the filtered list of statements + */ + public static List filterStatements(List statements, List currentTables) { + return statements.stream() + .filter(statement -> !currentTables.contains(tableFromStatement(statement))) + .collect(Collectors.toList()); + } } 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 e453160bc..17a9bfe37 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 @@ -28,34 +28,37 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.gson.reflect.TypeToken; - -import me.lucko.luckperms.common.actionlog.Log; +import me.lucko.luckperms.common.actionlog.LogPage; import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilterSqlBuilder; import me.lucko.luckperms.common.bulkupdate.BulkUpdate; +import me.lucko.luckperms.common.bulkupdate.BulkUpdateSqlBuilder; import me.lucko.luckperms.common.bulkupdate.BulkUpdateStatistics; -import me.lucko.luckperms.common.bulkupdate.PreparedStatementBuilder; -import me.lucko.luckperms.common.context.ContextSetJsonSerializer; +import me.lucko.luckperms.common.context.serializer.ContextSetJsonSerializer; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; +import me.lucko.luckperms.common.filter.sql.ConstraintSqlBuilder; 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.model.manager.group.GroupManager; -import me.lucko.luckperms.common.model.nodemap.MutateResult; import me.lucko.luckperms.common.node.factory.NodeBuilders; import me.lucko.luckperms.common.node.matcher.ConstraintNodeMatcher; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.StorageMetadata; import me.lucko.luckperms.common.storage.implementation.StorageImplementation; import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; import me.lucko.luckperms.common.storage.misc.NodeEntry; import me.lucko.luckperms.common.storage.misc.PlayerSaveResultImpl; +import me.lucko.luckperms.common.util.Difference; import me.lucko.luckperms.common.util.Uuids; import me.lucko.luckperms.common.util.gson.GsonProvider; - -import net.kyori.adventure.text.Component; import net.luckperms.api.actionlog.Action; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.MutableContextSet; import net.luckperms.api.model.PlayerSaveResult; import net.luckperms.api.node.Node; +import org.checkerframework.checker.nullness.qual.Nullable; import java.io.IOException; import java.io.InputStream; @@ -72,17 +75,18 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.function.Function; import java.util.stream.Collectors; public class SqlStorage implements StorageImplementation { private static final Type LIST_STRING_TYPE = new TypeToken>(){}.getType(); private static final String USER_PERMISSIONS_SELECT = "SELECT id, permission, value, server, world, expiry, contexts FROM '{prefix}user_permissions' WHERE uuid=?"; + private static final String USER_PERMISSIONS_SELECT_MULTIPLE = "SELECT uuid, id, permission, value, server, world, expiry, contexts FROM '{prefix}user_permissions' WHERE "; private static final String USER_PERMISSIONS_DELETE_SPECIFIC = "DELETE FROM '{prefix}user_permissions' WHERE id=?"; private static final String USER_PERMISSIONS_DELETE_SPECIFIC_PROPS = "DELETE FROM '{prefix}user_permissions' WHERE uuid=? AND permission=? AND value=? AND server=? AND world=? AND expiry=? AND contexts=?"; private static final String USER_PERMISSIONS_DELETE = "DELETE FROM '{prefix}user_permissions' WHERE uuid=?"; @@ -97,7 +101,8 @@ public class SqlStorage implements StorageImplementation { private static final String PLAYER_DELETE = "DELETE FROM '{prefix}players' WHERE uuid=?"; private static final String PLAYER_SELECT_ALL_UUIDS_BY_USERNAME = "SELECT uuid FROM '{prefix}players' WHERE username=? AND NOT uuid=?"; private static final String PLAYER_DELETE_ALL_UUIDS_BY_USERNAME = "DELETE FROM '{prefix}players' WHERE username=? AND NOT uuid=?"; - private static final String PLAYER_SELECT_BY_UUID = "SELECT username, primary_group FROM '{prefix}players' WHERE uuid=?"; + private static final String PLAYER_SELECT_BY_UUID = "SELECT username, primary_group FROM '{prefix}players' WHERE uuid=? LIMIT 1"; + private static final String PLAYER_SELECT_BY_UUID_MULTIPLE = "SELECT uuid, username, primary_group FROM '{prefix}players' WHERE "; private static final String PLAYER_SELECT_PRIMARY_GROUP_BY_UUID = "SELECT primary_group FROM '{prefix}players' WHERE uuid=? LIMIT 1"; private static final String PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID = "UPDATE '{prefix}players' SET primary_group=? WHERE uuid=?"; @@ -126,11 +131,12 @@ public class SqlStorage implements StorageImplementation { private static final String ACTION_INSERT = "INSERT INTO '{prefix}actions' (time, actor_uuid, actor_name, type, acted_uuid, acted_name, action) VALUES(?, ?, ?, ?, ?, ?, ?)"; private static final String ACTION_SELECT_ALL = "SELECT * FROM '{prefix}actions'"; + private static final String ACTION_COUNT = "SELECT COUNT(*) FROM '{prefix}actions'"; private final LuckPermsPlugin plugin; private final ConnectionFactory connectionFactory; - private final Function statementProcessor; + private final StatementProcessor statementProcessor; public SqlStorage(LuckPermsPlugin plugin, ConnectionFactory connectionFactory, String tablePrefix) { this.plugin = plugin; @@ -152,7 +158,7 @@ public ConnectionFactory getConnectionFactory() { return this.connectionFactory; } - public Function getStatementProcessor() { + public StatementProcessor getStatementProcessor() { return this.statementProcessor; } @@ -160,30 +166,32 @@ public Function getStatementProcessor() { public void init() throws Exception { this.connectionFactory.init(this.plugin); - boolean tableExists; + List tables; try (Connection c = this.connectionFactory.getConnection()) { - tableExists = tableExists(c, this.statementProcessor.apply("{prefix}user_permissions")); - } - - if (!tableExists) { - applySchema(); + tables = listTables(c); } + applySchema(tables); } - private void applySchema() throws IOException, SQLException { - List statements; + private void applySchema(List existingTables) throws IOException, SQLException { + String schemaFileName = "me/lucko/luckperms/schema/" + this.connectionFactory.getImplementationName().toLowerCase(Locale.ROOT) + ".sql"; - String schemaFileName = "me/lucko/luckperms/schema/" + this.connectionFactory.getImplementationName().toLowerCase() + ".sql"; + List statements; try (InputStream is = this.plugin.getBootstrap().getResourceStream(schemaFileName)) { if (is == null) { throw new IOException("Couldn't locate schema file for " + this.connectionFactory.getImplementationName()); } statements = SchemaReader.getStatements(is).stream() - .map(this.statementProcessor) + .map(this.statementProcessor::process) .collect(Collectors.toList()); } + statements = SchemaReader.filterStatements(statements, existingTables); + if (statements.isEmpty()) { + return; + } + try (Connection connection = this.connectionFactory.getConnection()) { boolean utf8mb4Unsupported = false; @@ -221,19 +229,19 @@ public void shutdown() { try { this.connectionFactory.shutdown(); } catch (Exception e) { - this.plugin.getLogger().severe("Exception whilst disabling SQLite storage", e); + this.plugin.getLogger().severe("Exception whilst disabling SQL storage", e); } } @Override - public Map getMeta() { + public StorageMetadata getMeta() { return this.connectionFactory.getMeta(); } @Override public void logAction(Action entry) throws SQLException { try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(ACTION_INSERT))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(ACTION_INSERT))) { writeAction(entry, ps); ps.execute(); } @@ -241,18 +249,38 @@ public void logAction(Action entry) throws SQLException { } @Override - public Log getLog() throws SQLException { - final Log.Builder log = Log.builder(); + public LogPage getLogPage(FilterList filter, @Nullable PageParameters page) throws SQLException { + int count = 0; + List content = new ArrayList<>(); + try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(ACTION_SELECT_ALL))) { + ActionFilterSqlBuilder countSqlBuilder = new ActionFilterSqlBuilder(); + countSqlBuilder.builder().append(ACTION_COUNT); + countSqlBuilder.visit(filter); + + try (PreparedStatement ps = countSqlBuilder.builder().build(c, this.statementProcessor)) { + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + count = rs.getInt(1); + } + } + } + + ActionFilterSqlBuilder sqlBuilder = new ActionFilterSqlBuilder(); + sqlBuilder.builder().append(ACTION_SELECT_ALL); + sqlBuilder.visit(filter); + sqlBuilder.builder().append(" ORDER BY time DESC, id DESC"); + sqlBuilder.visit(page); + + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { - log.add(readAction(rs)); + content.add(readAction(rs)); } } } } - return log.build(); + return LogPage.of(content, page, count); } @Override @@ -261,18 +289,20 @@ public void applyBulkUpdate(BulkUpdate bulkUpdate) throws SQLException { try (Connection c = this.connectionFactory.getConnection()) { if (bulkUpdate.getDataType().isIncludingUsers()) { - String table = this.statementProcessor.apply("{prefix}user_permissions"); - try (PreparedStatement ps = bulkUpdate.buildAsSql().build(c, q -> q.replace("{table}", table))) { + StatementProcessor tableReplacement = s -> s.replace("{table}", "{prefix}user_permissions"); + + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(bulkUpdate); + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor.compose(tableReplacement))) { if (bulkUpdate.isTrackingStatistics()) { - PreparedStatementBuilder builder = new PreparedStatementBuilder(); - builder.append(USER_PERMISSIONS_SELECT_DISTINCT); - bulkUpdate.appendConstraintsAsSql(builder); + BulkUpdateSqlBuilder statsSqlBuilder = new BulkUpdateSqlBuilder(); + statsSqlBuilder.builder().append(USER_PERMISSIONS_SELECT_DISTINCT); + statsSqlBuilder.visit(bulkUpdate.getFilters()); - try (PreparedStatement lookup = builder.build(c, this.statementProcessor)) { + try (PreparedStatement lookup = statsSqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = lookup.executeQuery()) { Set uuids = new HashSet<>(); - while (rs.next()) { uuids.add(Uuids.fromString(rs.getString("uuid"))); } @@ -280,7 +310,9 @@ public void applyBulkUpdate(BulkUpdate bulkUpdate) throws SQLException { stats.incrementAffectedUsers(uuids.size()); } } - stats.incrementAffectedNodes(ps.executeUpdate()); + + int rowsAffected = ps.executeUpdate(); + stats.incrementAffectedNodes(rowsAffected); } else { ps.execute(); } @@ -288,18 +320,20 @@ public void applyBulkUpdate(BulkUpdate bulkUpdate) throws SQLException { } if (bulkUpdate.getDataType().isIncludingGroups()) { - String table = this.statementProcessor.apply("{prefix}group_permissions"); - try (PreparedStatement ps = bulkUpdate.buildAsSql().build(c, q -> q.replace("{table}", table))) { + StatementProcessor tableReplacement = s -> s.replace("{table}", "{prefix}group_permissions"); + + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(bulkUpdate); + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor.compose(tableReplacement))) { if (bulkUpdate.isTrackingStatistics()) { - PreparedStatementBuilder builder = new PreparedStatementBuilder(); - builder.append(GROUP_PERMISSIONS_SELECT_ALL); - bulkUpdate.appendConstraintsAsSql(builder); + BulkUpdateSqlBuilder statsSqlBuilder = new BulkUpdateSqlBuilder(); + statsSqlBuilder.builder().append(GROUP_PERMISSIONS_SELECT_ALL); + statsSqlBuilder.visit(bulkUpdate.getFilters()); - try (PreparedStatement lookup = builder.build(c, this.statementProcessor)) { + try (PreparedStatement lookup = statsSqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = lookup.executeQuery()) { Set groups = new HashSet<>(); - while (rs.next()) { groups.add(rs.getString("name")); } @@ -307,7 +341,9 @@ public void applyBulkUpdate(BulkUpdate bulkUpdate) throws SQLException { stats.incrementAffectedGroups(groups.size()); } } - stats.incrementAffectedNodes(ps.executeUpdate()); + + int rowsAffected = ps.executeUpdate(); + stats.incrementAffectedNodes(rowsAffected); } else { ps.execute(); } @@ -318,16 +354,38 @@ public void applyBulkUpdate(BulkUpdate bulkUpdate) throws SQLException { @Override public User loadUser(UUID uniqueId, String username) throws SQLException { - User user = this.plugin.getUserManager().getOrMake(uniqueId, username); - List nodes; SqlPlayerData playerData; try (Connection c = this.connectionFactory.getConnection()) { - nodes = selectUserPermissions(c, user.getUniqueId()); - playerData = selectPlayerData(c, user.getUniqueId()); + nodes = selectUserPermissions(c, uniqueId); + playerData = selectPlayerData(c, uniqueId); + } + + return createUser(uniqueId, username, playerData, nodes, true); + } + + @Override + public Map loadUsers(Set uniqueIds) throws Exception { + Map> nodesMap; + Map playerDataMap; + + try (Connection c = this.connectionFactory.getConnection()) { + nodesMap = selectUserPermissions(c, uniqueIds); + playerDataMap = selectPlayerData(c, uniqueIds); } + Map users = new HashMap<>(); + for (UUID uniqueId : uniqueIds) { + SqlPlayerData playerData = playerDataMap.get(uniqueId); + List nodes = nodesMap.get(uniqueId); + users.put(uniqueId, createUser(uniqueId, null, playerData, nodes, false)); + } + return users; + } + + private User createUser(UUID uniqueId, String username, SqlPlayerData playerData, List nodes, boolean saveAfterAudit) throws SQLException { + User user = this.plugin.getUserManager().getOrMake(uniqueId, username); if (playerData != null) { if (playerData.primaryGroup != null) { user.getPrimaryGroup().setStoredValue(playerData.primaryGroup); @@ -341,7 +399,7 @@ public User loadUser(UUID uniqueId, String username) throws SQLException { user.loadNodesFromStorage(nodes); this.plugin.getUserManager().giveDefaultIfNeeded(user); - if (user.auditTemporaryNodes()) { + if (user.auditTemporaryNodes() && saveAfterAudit) { saveUser(user); } @@ -350,20 +408,27 @@ public User loadUser(UUID uniqueId, String username) throws SQLException { @Override public void saveUser(User user) throws SQLException { - MutateResult changes = user.normalData().exportChanges(results -> { + Difference changes = user.normalData().exportChanges(results -> { if (this.plugin.getUserManager().isNonDefaultUser(user)) { return true; } // if the only change is adding the default node, we don't need to export if (results.getChanges().size() == 1) { - MutateResult.Change onlyChange = results.getChanges().iterator().next(); - return !(onlyChange.getType() == MutateResult.ChangeType.ADD && this.plugin.getUserManager().isDefaultNode(onlyChange.getNode())); + Difference.Change onlyChange = results.getChanges().iterator().next(); + return !(onlyChange.type() == Difference.ChangeType.ADD && this.plugin.getUserManager().isDefaultNode(onlyChange.value())); } 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()); @@ -375,7 +440,7 @@ public void saveUser(User user) throws SQLException { updateUserPermissions(c, user.getUniqueId(), changes.getAdded(), changes.getRemoved()); insertPlayerData(c, user.getUniqueId(), new SqlPlayerData( user.getPrimaryGroup().getStoredValue().orElse(GroupManager.DEFAULT_GROUP_NAME), - user.getUsername().orElse("null").toLowerCase() + user.getUsername().orElse("null").toLowerCase(Locale.ROOT) )); } } @@ -384,7 +449,7 @@ public void saveUser(User user) throws SQLException { public Set getUniqueUsers() throws SQLException { Set uuids = new HashSet<>(); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_SELECT_DISTINCT))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(USER_PERMISSIONS_SELECT_DISTINCT))) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { UUID uuid = Uuids.fromString(rs.getString("uuid")); @@ -400,12 +465,14 @@ public Set getUniqueUsers() throws SQLException { @Override public List> searchUserNodes(ConstraintNodeMatcher constraint) throws SQLException { - PreparedStatementBuilder builder = new PreparedStatementBuilder().append(USER_PERMISSIONS_SELECT_PERMISSION); - constraint.getConstraint().appendSql(builder, "permission"); + ConstraintSqlBuilder sqlBuilder = new ConstraintSqlBuilder(); + sqlBuilder.builder().append(USER_PERMISSIONS_SELECT_PERMISSION); + sqlBuilder.builder().append("permission "); + sqlBuilder.visit(constraint.getConstraint()); List> held = new ArrayList<>(); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = builder.build(c, this.statementProcessor)) { + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { UUID holder = UUID.fromString(rs.getString("uuid")); @@ -430,7 +497,7 @@ public List> searchUserNodes(ConstraintNodeM public Group createAndLoadGroup(String name) throws SQLException { String query = GROUP_INSERT.getOrDefault(this.connectionFactory.getImplementationName(), GROUP_INSERT_DEFAULT); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(query))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(query))) { ps.setString(1, name); ps.execute(); } @@ -479,7 +546,7 @@ public void loadAllGroups() throws SQLException { @Override public void saveGroup(Group group) throws SQLException { - MutateResult changes = group.normalData().exportChanges(c -> true); + Difference changes = group.normalData().exportChanges(c -> true); if (!changes.isEmpty()) { try (Connection c = this.connectionFactory.getConnection()) { @@ -493,7 +560,7 @@ public void deleteGroup(Group group) throws SQLException { try (Connection c = this.connectionFactory.getConnection()) { deleteGroupPermissions(c, group.getName()); - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_DELETE))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(GROUP_DELETE))) { ps.setString(1, group.getName()); ps.execute(); } @@ -504,12 +571,14 @@ public void deleteGroup(Group group) throws SQLException { @Override public List> searchGroupNodes(ConstraintNodeMatcher constraint) throws SQLException { - PreparedStatementBuilder builder = new PreparedStatementBuilder().append(GROUP_PERMISSIONS_SELECT_PERMISSION); - constraint.getConstraint().appendSql(builder, "permission"); + ConstraintSqlBuilder sqlBuilder = new ConstraintSqlBuilder(); + sqlBuilder.builder().append(GROUP_PERMISSIONS_SELECT_PERMISSION); + sqlBuilder.builder().append("permission "); + sqlBuilder.visit(constraint.getConstraint()); List> held = new ArrayList<>(); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = builder.build(c, this.statementProcessor)) { + try (PreparedStatement ps = sqlBuilder.builder().build(c, this.statementProcessor)) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { String holder = rs.getString("name"); @@ -594,7 +663,7 @@ public void saveTrack(Track track) throws SQLException { @Override public void deleteTrack(Track track) throws SQLException { try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_DELETE))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(TRACK_DELETE))) { ps.setString(1, track.getName()); ps.execute(); } @@ -605,27 +674,26 @@ public void deleteTrack(Track track) throws SQLException { @Override public PlayerSaveResult savePlayerData(UUID uniqueId, String username) throws SQLException { - username = username.toLowerCase(); - - // find any existing mapping - String oldUsername = getPlayerName(uniqueId); + username = username.toLowerCase(Locale.ROOT); + String oldUsername = null; - // do the insert - if (!username.equals(oldUsername)) { - try (Connection c = this.connectionFactory.getConnection()) { - if (oldUsername != null) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_UPDATE_USERNAME_FOR_UUID))) { + try (Connection c = this.connectionFactory.getConnection()) { + SqlPlayerData existingPlayerData = selectPlayerData(c, uniqueId); + if (existingPlayerData == null) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_INSERT))) { + ps.setString(1, uniqueId.toString()); + ps.setString(2, username); + ps.setString(3, GroupManager.DEFAULT_GROUP_NAME); + ps.execute(); + } + } else { + oldUsername = existingPlayerData.username; + if (!username.equals(oldUsername)) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_UPDATE_USERNAME_FOR_UUID))) { ps.setString(1, username); ps.setString(2, uniqueId.toString()); ps.execute(); } - } else { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_INSERT))) { - ps.setString(1, uniqueId.toString()); - ps.setString(2, username); - ps.setString(3, GroupManager.DEFAULT_GROUP_NAME); - ps.execute(); - } } } } @@ -634,7 +702,7 @@ public PlayerSaveResult savePlayerData(UUID uniqueId, String username) throws SQ Set conflicting = new HashSet<>(); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_ALL_UUIDS_BY_USERNAME))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_SELECT_ALL_UUIDS_BY_USERNAME))) { ps.setString(1, username); ps.setString(2, uniqueId.toString()); try (ResultSet rs = ps.executeQuery()) { @@ -648,7 +716,7 @@ public PlayerSaveResult savePlayerData(UUID uniqueId, String username) throws SQ if (!conflicting.isEmpty()) { // remove the mappings for conflicting uuids try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_DELETE_ALL_UUIDS_BY_USERNAME))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_DELETE_ALL_UUIDS_BY_USERNAME))) { ps.setString(1, username); ps.setString(2, uniqueId.toString()); ps.execute(); @@ -663,7 +731,7 @@ public PlayerSaveResult savePlayerData(UUID uniqueId, String username) throws SQ @Override public void deletePlayerData(UUID uniqueId) throws SQLException { try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_DELETE))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_DELETE))) { ps.setString(1, uniqueId.toString()); ps.execute(); } @@ -672,9 +740,9 @@ public void deletePlayerData(UUID uniqueId) throws SQLException { @Override public UUID getPlayerUniqueId(String username) throws SQLException { - username = username.toLowerCase(); + username = username.toLowerCase(Locale.ROOT); try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_UUID_BY_USERNAME))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_SELECT_UUID_BY_USERNAME))) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { @@ -689,11 +757,14 @@ public UUID getPlayerUniqueId(String username) throws SQLException { @Override public String getPlayerName(UUID uniqueId) throws SQLException { try (Connection c = this.connectionFactory.getConnection()) { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_USERNAME_BY_UUID))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_SELECT_USERNAME_BY_UUID))) { ps.setString(1, uniqueId.toString()); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { - return rs.getString("username"); + String username = rs.getString("username"); + if (username != null && !username.equals("null")) { + return username; + } } } } @@ -705,7 +776,7 @@ private static void writeAction(Action action, PreparedStatement ps) throws SQLE ps.setLong(1, action.getTimestamp().getEpochSecond()); ps.setString(2, action.getSource().getUniqueId().toString()); ps.setString(3, action.getSource().getName()); - ps.setString(4, Character.toString(LoggedAction.getTypeCharacter(action.getTarget().getType()))); + ps.setString(4, LoggedAction.getTypeString(action.getTarget().getType())); ps.setString(5, action.getTarget().getUniqueId().map(UUID::toString).orElse("null")); ps.setString(6, action.getTarget().getName()); ps.setString(7, action.getDescription()); @@ -802,7 +873,7 @@ private void updatePermissions(Connection c, String holder, Set add, Set add, Set add, Set add, Set selectUserPermissions(Connection c, UUID user) throws SQLException { List nodes = new ArrayList<>(); - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_SELECT))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(USER_PERMISSIONS_SELECT))) { ps.setString(1, user.toString()); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Node node = readNode(rs); if (node != null) { - nodes.add(readNode(rs)); + nodes.add(node); } } } @@ -851,7 +922,7 @@ private List selectUserPermissions(Connection c, UUID user) throws SQLExce } private SqlPlayerData selectPlayerData(Connection c, UUID user) throws SQLException { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_BY_UUID))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_SELECT_BY_UUID))) { ps.setString(1, user.toString()); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { @@ -863,12 +934,64 @@ private SqlPlayerData selectPlayerData(Connection c, UUID user) throws SQLExcept } } + private Map> selectUserPermissions(Connection c, Set users) throws SQLException { + Map> map = new HashMap<>(); + for (UUID uuid : users) { + map.put(uuid, new ArrayList<>()); + } + + try (Statement s = c.createStatement()) { + String sql = createUserSelectWhereClause(USER_PERMISSIONS_SELECT_MULTIPLE, users); + try (ResultSet rs = s.executeQuery(sql)) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("uuid")); + Node node = readNode(rs); + if (node != null) { + map.get(uuid).add(node); + } + } + } + } + + return map; + } + + private Map selectPlayerData(Connection c, Set users) throws SQLException { + Map map = new HashMap<>(); + + try (Statement s = c.createStatement()) { + String sql = createUserSelectWhereClause(PLAYER_SELECT_BY_UUID_MULTIPLE, users); + try (ResultSet rs = s.executeQuery(sql)) { + while (rs.next()) { + UUID uuid = UUID.fromString(rs.getString("uuid")); + SqlPlayerData data = new SqlPlayerData( + rs.getString("primary_group"), + rs.getString("username") + ); + map.put(uuid, data); + } + } + } + + return map; + } + + private String createUserSelectWhereClause(String baseQuery, Set users) { + String param = users.stream() + .map(uuid -> "'" + uuid + "'") + .collect(Collectors.joining(",", "uuid IN (", ")")); + + // we don't want to use preparedstatements because the parameter length is variable + // safe to do string concat/replacement because the UUID.toString value isn't injectable + return this.statementProcessor.process(baseQuery) + param; + } + private void deleteUser(Connection c, UUID user) throws SQLException { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(USER_PERMISSIONS_DELETE))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(USER_PERMISSIONS_DELETE))) { ps.setString(1, user.toString()); ps.execute(); } - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) { ps.setString(1, GroupManager.DEFAULT_GROUP_NAME); ps.setString(2, user.toString()); ps.execute(); @@ -877,7 +1000,7 @@ private void deleteUser(Connection c, UUID user) throws SQLException { private void insertPlayerData(Connection c, UUID user, SqlPlayerData data) throws SQLException { boolean hasPrimaryGroupSaved; - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_SELECT_PRIMARY_GROUP_BY_UUID))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_SELECT_PRIMARY_GROUP_BY_UUID))) { ps.setString(1, user.toString()); try (ResultSet rs = ps.executeQuery()) { hasPrimaryGroupSaved = rs.next(); @@ -886,14 +1009,14 @@ private void insertPlayerData(Connection c, UUID user, SqlPlayerData data) throw if (hasPrimaryGroupSaved) { // update - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_UPDATE_PRIMARY_GROUP_BY_UUID))) { ps.setString(1, data.primaryGroup); ps.setString(2, user.toString()); ps.execute(); } } else { // insert - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(PLAYER_INSERT))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(PLAYER_INSERT))) { ps.setString(1, user.toString()); ps.setString(2, data.username); ps.setString(3, data.primaryGroup); @@ -904,10 +1027,10 @@ private void insertPlayerData(Connection c, UUID user, SqlPlayerData data) throw private Set selectGroups(Connection c) throws SQLException { Set groups = new HashSet<>(); - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_SELECT_ALL))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(GROUP_SELECT_ALL))) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { - groups.add(rs.getString("name").toLowerCase()); + groups.add(rs.getString("name").toLowerCase(Locale.ROOT)); } } } @@ -916,7 +1039,7 @@ private Set selectGroups(Connection c) throws SQLException { private List selectGroupPermissions(Connection c, String group) throws SQLException { List nodes = new ArrayList<>(); - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_SELECT))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(GROUP_PERMISSIONS_SELECT))) { ps.setString(1, group); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { @@ -931,7 +1054,7 @@ private List selectGroupPermissions(Connection c, String group) throws SQL } private void selectAllGroupPermissions(Map> nodes, Connection c) throws SQLException { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_SELECT_ALL))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(GROUP_PERMISSIONS_SELECT_ALL))) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { String holder = rs.getString("name"); @@ -939,7 +1062,7 @@ private void selectAllGroupPermissions(Map> nodes, Conn if (list != null) { Node node = readNode(rs); if (node != null) { - list.add(readNode(rs)); + list.add(node); } } } @@ -948,7 +1071,7 @@ private void selectAllGroupPermissions(Map> nodes, Conn } private void deleteGroupPermissions(Connection c, String group) throws SQLException { - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(GROUP_PERMISSIONS_DELETE))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(GROUP_PERMISSIONS_DELETE))) { ps.setString(1, group); ps.execute(); } @@ -956,7 +1079,7 @@ private void deleteGroupPermissions(Connection c, String group) throws SQLExcept private List selectTrack(Connection c, String name) throws SQLException { String groups; - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_SELECT))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(TRACK_SELECT))) { ps.setString(1, name); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { @@ -971,7 +1094,7 @@ private List selectTrack(Connection c, String name) throws SQLException private void insertTrack(Connection c, String name, List groups) throws SQLException { String json = GsonProvider.normal().toJson(groups); - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_INSERT))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(TRACK_INSERT))) { ps.setString(1, name); ps.setString(2, json); ps.execute(); @@ -980,7 +1103,7 @@ private void insertTrack(Connection c, String name, List groups) throws private void updateTrack(Connection c, String name, List groups) throws SQLException { String json = GsonProvider.normal().toJson(groups); - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_UPDATE))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(TRACK_UPDATE))) { ps.setString(1, json); ps.setString(2, name); ps.execute(); @@ -989,25 +1112,24 @@ private void updateTrack(Connection c, String name, List groups) throws private Set selectTracks(Connection c) throws SQLException { Set tracks = new HashSet<>(); - try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.apply(TRACK_SELECT_ALL))) { + try (PreparedStatement ps = c.prepareStatement(this.statementProcessor.process(TRACK_SELECT_ALL))) { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { - tracks.add(rs.getString("name").toLowerCase()); + tracks.add(rs.getString("name").toLowerCase(Locale.ROOT)); } } } return tracks; } - private static boolean tableExists(Connection connection, String table) throws SQLException { + private static List listTables(Connection connection) throws SQLException { + List tables = new ArrayList<>(); try (ResultSet rs = connection.getMetaData().getTables(connection.getCatalog(), null, "%", null)) { while (rs.next()) { - if (rs.getString(3).equalsIgnoreCase(table)) { - return true; - } + tables.add(rs.getString(3).toLowerCase(Locale.ROOT)); } - return false; } + return tables; } private static final class SqlPlayerData { diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/StatementProcessor.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/StatementProcessor.java new file mode 100644 index 000000000..2cf80ea5b --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/StatementProcessor.java @@ -0,0 +1,43 @@ +/* + * 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.storage.implementation.sql; + +import java.util.Objects; + +public interface StatementProcessor { + + StatementProcessor USE_BACKTICKS = s -> s.replace('\'', '`'); + + StatementProcessor USE_DOUBLE_QUOTES = s -> s.replace('\'', '"'); + + String process(String statement); + + default StatementProcessor compose(StatementProcessor before) { + Objects.requireNonNull(before); + return s -> process(before.process(s)); + } + +} diff --git a/sponge/sponge-service-api7/src/main/java/org/spongepowered/api/event/permission/SubjectDataUpdateEvent.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/AbstractSqlBuilder.java similarity index 76% rename from sponge/sponge-service-api7/src/main/java/org/spongepowered/api/event/permission/SubjectDataUpdateEvent.java rename to common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/AbstractSqlBuilder.java index 987acaeb3..a42d27cd4 100644 --- a/sponge/sponge-service-api7/src/main/java/org/spongepowered/api/event/permission/SubjectDataUpdateEvent.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/AbstractSqlBuilder.java @@ -23,14 +23,14 @@ * SOFTWARE. */ -package org.spongepowered.api.event.permission; +package me.lucko.luckperms.common.storage.implementation.sql.builder; -import org.spongepowered.api.event.Event; -import org.spongepowered.api.service.permission.SubjectData; +public class AbstractSqlBuilder { -// Copy of https://github.com/SpongePowered/SpongeAPI/blob/api-8/src/main/java/org/spongepowered/api/event/permission/SubjectDataUpdateEvent.java -public interface SubjectDataUpdateEvent extends Event { + protected final PreparedStatementBuilder builder = new PreparedStatementBuilder(); - SubjectData getUpdatedData(); + public PreparedStatementBuilder builder() { + return this.builder; + } -} \ No newline at end of file +} diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/PreparedStatementBuilder.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/PreparedStatementBuilder.java similarity index 69% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/PreparedStatementBuilder.java rename to common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/PreparedStatementBuilder.java index 0e076c0a7..7c1ad3f18 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/PreparedStatementBuilder.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/builder/PreparedStatementBuilder.java @@ -23,14 +23,15 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.bulkupdate; +package me.lucko.luckperms.common.storage.implementation.sql.builder; + +import me.lucko.luckperms.common.storage.implementation.sql.StatementProcessor; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; public class PreparedStatementBuilder { private final StringBuilder sb = new StringBuilder(); @@ -56,13 +57,22 @@ public PreparedStatementBuilder variable(String variable) { return this; } - public PreparedStatement build(Connection connection, Function mapping) throws SQLException { - PreparedStatement statement = connection.prepareStatement(mapping.apply(this.sb.toString())); - for (int i = 0; i < this.variables.size(); i++) { - String var = this.variables.get(i); - statement.setString(i + 1, var); + public PreparedStatement build(Connection connection, StatementProcessor processor) throws SQLException { + PreparedStatement statement = null; + try { + statement = connection.prepareStatement(processor.process(this.sb.toString())); + for (int i = 0; i < this.variables.size(); i++) { + String var = this.variables.get(i); + statement.setString(i + 1, var); + } + return statement; + } catch (SQLException e) { + // if an exception is thrown, any try-with-resources block above this call won't be able to close the statement + if (statement != null) { + statement.close(); + } + throw e; } - return statement; } public String toReadableString() { @@ -72,4 +82,8 @@ public String toReadableString() { } return s; } + + public String toQueryString() { + return this.sb.toString(); + } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/ConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/ConnectionFactory.java index 27e84bcd4..52adbac3a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/ConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/ConnectionFactory.java @@ -26,14 +26,11 @@ package me.lucko.luckperms.common.storage.implementation.sql.connection; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - -import net.kyori.adventure.text.Component; +import me.lucko.luckperms.common.storage.StorageMetadata; +import me.lucko.luckperms.common.storage.implementation.sql.StatementProcessor; import java.sql.Connection; import java.sql.SQLException; -import java.util.Collections; -import java.util.Map; -import java.util.function.Function; public interface ConnectionFactory { @@ -43,11 +40,9 @@ public interface ConnectionFactory { void shutdown() throws Exception; - default Map getMeta() { - return Collections.emptyMap(); - } + StorageMetadata getMeta(); - Function getStatementProcessor(); + StatementProcessor getStatementProcessor(); Connection getConnection() throws SQLException; diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/FlatfileConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/FlatfileConnectionFactory.java index a80c7edee..abe027947 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/FlatfileConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/FlatfileConnectionFactory.java @@ -25,26 +25,19 @@ package me.lucko.luckperms.common.storage.implementation.sql.connection.file; +import me.lucko.luckperms.common.storage.StorageMetadata; import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.sql.Connection; import java.sql.SQLException; -import java.text.DecimalFormat; -import java.util.Collections; -import java.util.Map; /** * Abstract {@link ConnectionFactory} using a file based database driver. */ abstract class FlatfileConnectionFactory implements ConnectionFactory { - /** Format used for formatting database file size. */ - protected static final DecimalFormat FILE_SIZE_FORMAT = new DecimalFormat("#.##"); /** The current open connection, if any */ private NonClosableConnection connection; @@ -102,26 +95,19 @@ protected void migrateOldDatabaseFile(String oldName) { } @Override - public Map getMeta() { - String fileSize; + public StorageMetadata getMeta() { + StorageMetadata metadata = new StorageMetadata(); + Path databaseFile = getWriteFile(); if (Files.exists(databaseFile)) { - long length; try { - length = Files.size(databaseFile); + long length = Files.size(databaseFile); + metadata.sizeBytes(length); } catch (IOException e) { - length = 0; + // ignore } - - double size = length / 1048576D; - fileSize = FILE_SIZE_FORMAT.format(size) + "MB"; - } else { - fileSize = "0MB"; } - return Collections.singletonMap( - Component.translatable("luckperms.command.info.storage.meta.file-size-key"), - Component.text(fileSize, NamedTextColor.GREEN) - ); + return metadata; } } 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 ddae98fa8..2764e3bba 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 @@ -26,18 +26,27 @@ package me.lucko.luckperms.common.storage.implementation.sql.connection.file; import me.lucko.luckperms.common.dependencies.Dependency; -import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.implementation.sql.StatementProcessor; +import java.io.IOException; import java.lang.reflect.Constructor; +import java.nio.file.Files; import java.nio.file.Path; import java.sql.Connection; import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; import java.util.EnumSet; import java.util.Properties; -import java.util.function.Function; public class H2ConnectionFactory extends FlatfileConnectionFactory { + public static final StatementProcessor STATEMENT_PROCESSOR = s -> s + .replace('\'', '`') + .replace("LIKE", "ILIKE") + .replace("value", "`value`") + .replace("``value``", "`value`"); + private Constructor connectionConstructor; public H2ConnectionFactory(Path file) { @@ -53,19 +62,25 @@ public String getImplementationName() { public void init(LuckPermsPlugin plugin) { migrateOldDatabaseFile("luckperms.db.mv.db"); - IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER)); + ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER)); try { Class connectionClass = classLoader.loadClass("org.h2.jdbc.JdbcConnection"); - this.connectionConstructor = connectionClass.getConstructor(String.class, Properties.class); + this.connectionConstructor = connectionClass.getConstructor(String.class, Properties.class, String.class, Object.class, boolean.class); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } + + try { + new MigrateH2ToVersion2(plugin, super.getWriteFile().getParent()).run(this); + } catch (Exception e) { + plugin.getLogger().warn("Something went wrong whilst upgrading the LuckPerms database. Please report this on GitHub.", e); + } } @Override protected Connection createConnection(Path file) throws SQLException { try { - return (Connection) this.connectionConstructor.newInstance("jdbc:h2:" + file.toString(), new Properties()); + return (Connection) this.connectionConstructor.newInstance("jdbc:h2:" + file.toString(), new Properties(), null, null, false); } catch (ReflectiveOperationException e) { if (e.getCause() instanceof SQLException) { throw (SQLException) e.getCause(); @@ -82,7 +97,83 @@ protected Path getWriteFile() { } @Override - public Function getStatementProcessor() { - return s -> s.replace('\'', '`').replace("LIKE", "ILIKE"); + public StatementProcessor getStatementProcessor() { + return STATEMENT_PROCESSOR; + } + + /** + * Migrates the old (version 1) H2 database to version 2. + * + * See here for more info. + */ + private static final class MigrateH2ToVersion2 { + private final LuckPermsPlugin plugin; + private final Path directory; + + MigrateH2ToVersion2(LuckPermsPlugin plugin, Path directory) { + this.plugin = plugin; + this.directory = directory; + } + + public void run(H2ConnectionFactory newFactory) throws Exception { + Path oldDatabase = this.directory.resolve("luckperms-h2"); + Path oldDatabaseWriteFile = this.directory.resolve("luckperms-h2.mv.db"); + + if (!Files.exists(oldDatabaseWriteFile)) { + return; + } + + Path tempMigrationFile = this.directory.resolve("luckperms-h2-migration.sql"); + + 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)) { + try (Statement stmt = c.createStatement()) { + stmt.execute(String.format("SCRIPT TO '%s'", tempMigrationFile)); + } + } + + this.plugin.getLogger().info("[DB Upgrade] Stage 2: Importing the intermediary file into the new database..."); + try (Connection c = newFactory.getConnection()) { + try (Statement stmt = c.createStatement()) { + stmt.execute(String.format("RUNSCRIPT FROM '%s'", tempMigrationFile)); + } + } + + this.plugin.getLogger().info("[DB Upgrade] Stage 3: Tidying up..."); + Files.deleteIfExists(tempMigrationFile); + Files.move(oldDatabaseWriteFile, this.directory.resolve("luckperms-h2-v1-backup.mv.db")); + + this.plugin.getLogger().info("[DB Upgrade] All done!"); + } + + private Constructor getConnectionConstructor() { + this.plugin.getDependencyManager().loadDependencies(Collections.singleton(Dependency.H2_DRIVER_LEGACY)); + ClassLoader classLoader = this.plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.H2_DRIVER_LEGACY)); + try { + Class connectionClass = classLoader.loadClass("org.h2.jdbc.JdbcConnection"); + return connectionClass.getConstructor(String.class, Properties.class); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private Connection getConnection(Constructor constructor, Path file) { + try { + return (Connection) constructor.newInstance("jdbc:h2:" + file.toString(), new Properties()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } } + + } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java index 85c82036a..9d4e5b896 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/file/SqliteConnectionFactory.java @@ -26,8 +26,8 @@ package me.lucko.luckperms.common.storage.implementation.sql.connection.file; import me.lucko.luckperms.common.dependencies.Dependency; -import me.lucko.luckperms.common.dependencies.classloader.IsolatedClassLoader; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.implementation.sql.StatementProcessor; import java.lang.reflect.Constructor; import java.nio.file.Path; @@ -35,7 +35,6 @@ import java.sql.SQLException; import java.util.EnumSet; import java.util.Properties; -import java.util.function.Function; public class SqliteConnectionFactory extends FlatfileConnectionFactory { private Constructor connectionConstructor; @@ -53,7 +52,7 @@ public String getImplementationName() { public void init(LuckPermsPlugin plugin) { migrateOldDatabaseFile("luckperms.sqlite"); - IsolatedClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER)); + ClassLoader classLoader = plugin.getDependencyManager().obtainClassLoaderWith(EnumSet.of(Dependency.SQLITE_DRIVER)); try { Class connectionClass = classLoader.loadClass("org.sqlite.jdbc4.JDBC4Connection"); this.connectionConstructor = connectionClass.getConstructor(String.class, String.class, Properties.class); @@ -75,7 +74,7 @@ protected Connection createConnection(Path file) throws SQLException { } @Override - public Function getStatementProcessor() { - return s -> s.replace('\'', '`'); + public StatementProcessor getStatementProcessor() { + return StatementProcessor.USE_BACKTICKS; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/DriverBasedHikariConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/DriverBasedHikariConnectionFactory.java new file mode 100644 index 000000000..fef63dca3 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/DriverBasedHikariConnectionFactory.java @@ -0,0 +1,80 @@ +/* + * 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.storage.implementation.sql.connection.hikari; + +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; + +/** + * Extension of {@link HikariConnectionFactory} that uses the driver class name to configure Hikari. + */ +public abstract class DriverBasedHikariConnectionFactory extends HikariConnectionFactory { + protected DriverBasedHikariConnectionFactory(StorageCredentials configuration) { + super(configuration); + } + + protected abstract String driverClassName(); + + protected abstract String driverJdbcIdentifier(); + + @Override + protected void configureDatabase(HikariConfig config, String address, int port, String databaseName, String username, String password) { + config.setDriverClassName(driverClassName()); + config.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s", driverJdbcIdentifier(), address, port, databaseName)); + config.setUsername(username); + config.setPassword(password); + } + + @Override + protected void postInitialize() { + super.postInitialize(); + + // Calling Class.forName("") is enough to call the static initializer + // which makes our driver available in DriverManager. We don't want that, so unregister it after + // the pool has been setup. + deregisterDriver(driverClassName()); + } + + private static void deregisterDriver(String driverClassName) { + Enumeration drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver driver = drivers.nextElement(); + if (driver.getClass().getName().equals(driverClassName)) { + try { + DriverManager.deregisterDriver(driver); + } catch (SQLException e) { + // ignore + } + } + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/HikariConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/HikariConnectionFactory.java index 21c560d69..8e9d28cce 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/HikariConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/HikariConnectionFactory.java @@ -28,21 +28,17 @@ import com.google.common.collect.ImmutableList; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; - -import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.logging.PluginLogger; +import me.lucko.luckperms.common.storage.StorageMetadata; import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; import me.lucko.luckperms.common.storage.misc.StorageCredentials; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; +import me.lucko.luckperms.common.util.HostAndPort; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -63,7 +59,7 @@ public HikariConnectionFactory(StorageCredentials configuration) { * * @return the default port */ - protected abstract String defaultPort(); + protected abstract int defaultPort(); /** * Configures the {@link HikariConfig} with the relevant database properties. @@ -77,14 +73,14 @@ public HikariConnectionFactory(StorageCredentials configuration) { * @param username the database username * @param password the database password */ - protected abstract void configureDatabase(HikariConfig config, String address, String port, String databaseName, String username, String password); + protected abstract void configureDatabase(HikariConfig config, String address, int port, String databaseName, String username, String password); /** * Allows the connection factory instance to override certain properties before they are set. * * @param properties the current properties */ - protected void overrideProperties(Map properties) { + protected void overrideProperties(Map properties) { // https://github.com/brettwooldridge/HikariCP/wiki/Rapid-Recovery properties.putIfAbsent("socketTimeout", String.valueOf(TimeUnit.SECONDS.toMillis(30))); } @@ -95,8 +91,8 @@ protected void overrideProperties(Map properties) { * @param config the hikari config * @param properties the properties */ - protected void setProperties(HikariConfig config, Map properties) { - for (Map.Entry property : properties.entrySet()) { + protected void setProperties(HikariConfig config, Map properties) { + for (Map.Entry property : properties.entrySet()) { config.addDataSourceProperty(property.getKey(), property.getValue()); } } @@ -122,9 +118,12 @@ public void init(LuckPermsPlugin plugin) { config.setPoolName("luckperms-hikari"); // get the database info/credentials from the config file - String[] addressSplit = this.configuration.getAddress().split(":"); - String address = addressSplit[0]; - String port = addressSplit.length > 1 ? addressSplit[1] : defaultPort(); + HostAndPort hostAndPort = new HostAndPort(this.configuration.getAddress()) + .requireBracketsForIPv6() + .withDefaultPort(defaultPort()); + + String address = hostAndPort.getHost(); + int port = hostAndPort.getPort(); // allow the implementation to configure the HikariConfig appropriately with these values try { @@ -134,7 +133,7 @@ public void init(LuckPermsPlugin plugin) { } // get the extra connection properties from the config - Map properties = new HashMap<>(this.configuration.getProperties()); + Map properties = new HashMap<>(this.configuration.getProperties()); // allow the implementation to override/make changes to these properties overrideProperties(properties); @@ -180,11 +179,12 @@ public Connection getConnection() throws SQLException { } @Override - public Map getMeta() { - Map meta = new LinkedHashMap<>(); - boolean success = true; + public StorageMetadata getMeta() { + StorageMetadata metadata = new StorageMetadata(); + boolean success = true; long start = System.currentTimeMillis(); + try (Connection c = getConnection()) { try (Statement s = c.createStatement()) { s.execute("/* ping */ SELECT 1"); @@ -194,18 +194,12 @@ public Map getMeta() { } if (success) { - long duration = System.currentTimeMillis() - start; - meta.put( - Component.translatable("luckperms.command.info.storage.meta.ping-key"), - Component.text(duration + "ms", NamedTextColor.GREEN) - ); + int duration = (int) (System.currentTimeMillis() - start); + metadata.ping(duration); } - meta.put( - Component.translatable("luckperms.command.info.storage.meta.connected-key"), - Message.formatBoolean(success) - ); - return meta; + metadata.connected(success); + return metadata; } // dumb plugins seem to keep doing stupid stuff with shading of SLF4J and Log4J. 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 092e36f74..f9fbfe1d7 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 @@ -25,15 +25,10 @@ package me.lucko.luckperms.common.storage.implementation.sql.connection.hikari; -import com.zaxxer.hikari.HikariConfig; - +import me.lucko.luckperms.common.storage.implementation.sql.StatementProcessor; 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 class MariaDbConnectionFactory extends DriverBasedHikariConnectionFactory { public MariaDbConnectionFactory(StorageCredentials configuration) { super(configuration); } @@ -44,33 +39,22 @@ public String getImplementationName() { } @Override - protected String defaultPort() { - return "3306"; + protected int defaultPort() { + return 3306; } @Override - protected void configureDatabase(HikariConfig config, String address, String port, String databaseName, String username, String password) { - config.setDataSourceClassName("org.mariadb.jdbc.MariaDbDataSource"); - config.addDataSourceProperty("serverName", address); - config.addDataSourceProperty("port", port); - config.addDataSourceProperty("databaseName", databaseName); - config.setUsername(username); - config.setPassword(password); + protected String driverClassName() { + return "org.mariadb.jdbc.Driver"; } @Override - protected void setProperties(HikariConfig config, Map properties) { - String propertiesString = properties.entrySet().stream() - .map(e -> e.getKey() + "=" + e.getValue()) - .collect(Collectors.joining(";")); - - // kinda hacky. this will call #setProperties on the datasource, which will append these options - // onto the connections. - config.addDataSourceProperty("properties", propertiesString); + protected String driverJdbcIdentifier() { + return "mariadb"; } @Override - public Function getStatementProcessor() { - return s -> s.replace('\'', '`'); // use backticks for quotes + public StatementProcessor getStatementProcessor() { + return StatementProcessor.USE_BACKTICKS; } } 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 4269fff96..53c59dd56 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 @@ -25,18 +25,12 @@ package me.lucko.luckperms.common.storage.implementation.sql.connection.hikari; -import com.zaxxer.hikari.HikariConfig; - +import me.lucko.luckperms.common.storage.implementation.sql.StatementProcessor; 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; -public class MySqlConnectionFactory extends HikariConnectionFactory { +public class MySqlConnectionFactory extends DriverBasedHikariConnectionFactory { public MySqlConnectionFactory(StorageCredentials configuration) { super(configuration); } @@ -47,40 +41,22 @@ public String getImplementationName() { } @Override - protected String defaultPort() { - return "3306"; + protected int defaultPort() { + return 3306; } @Override - protected void configureDatabase(HikariConfig config, String address, String port, String databaseName, String username, String password) { - config.setDriverClassName("com.mysql.cj.jdbc.Driver"); - config.setJdbcUrl("jdbc:mysql://" + address + ":" + port + "/" + databaseName); - config.setUsername(username); - config.setPassword(password); + protected String driverClassName() { + return "com.mysql.cj.jdbc.Driver"; } @Override - protected void postInitialize() { - super.postInitialize(); - - // Calling Class.forName("com.mysql.cj.jdbc.Driver") is enough to call the static initializer - // which makes our driver available in DriverManager. We don't want that, so unregister it after - // the pool has been setup. - Enumeration drivers = DriverManager.getDrivers(); - while (drivers.hasMoreElements()) { - Driver driver = drivers.nextElement(); - if (driver.getClass().getName().equals("com.mysql.cj.jdbc.Driver")) { - try { - DriverManager.deregisterDriver(driver); - } catch (SQLException e) { - // ignore - } - } - } + protected String driverJdbcIdentifier() { + return "mysql"; } @Override - protected void overrideProperties(Map properties) { + protected void overrideProperties(Map properties) { // https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration properties.putIfAbsent("cachePrepStmts", "true"); properties.putIfAbsent("prepStmtCacheSize", "250"); @@ -104,7 +80,7 @@ protected void overrideProperties(Map properties) { } @Override - public Function getStatementProcessor() { - return s -> s.replace('\'', '`'); // use backticks for quotes + public StatementProcessor getStatementProcessor() { + return StatementProcessor.USE_BACKTICKS; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgreConnectionFactory.java b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgresConnectionFactory.java similarity index 65% rename from common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgreConnectionFactory.java rename to common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgresConnectionFactory.java index 2bdf4cf51..7e24c2aaf 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgreConnectionFactory.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/implementation/sql/connection/hikari/PostgresConnectionFactory.java @@ -25,15 +25,13 @@ package me.lucko.luckperms.common.storage.implementation.sql.connection.hikari; -import com.zaxxer.hikari.HikariConfig; - +import me.lucko.luckperms.common.storage.implementation.sql.StatementProcessor; import me.lucko.luckperms.common.storage.misc.StorageCredentials; import java.util.Map; -import java.util.function.Function; -public class PostgreConnectionFactory extends HikariConnectionFactory { - public PostgreConnectionFactory(StorageCredentials configuration) { +public class PostgresConnectionFactory extends DriverBasedHikariConnectionFactory { + public PostgresConnectionFactory(StorageCredentials configuration) { super(configuration); } @@ -43,22 +41,22 @@ public String getImplementationName() { } @Override - protected String defaultPort() { - return "5432"; + protected int defaultPort() { + return 5432; + } + + @Override + protected String driverClassName() { + return "org.postgresql.Driver"; } @Override - protected void configureDatabase(HikariConfig config, String address, String port, String databaseName, String username, String password) { - config.setDataSourceClassName("org.postgresql.ds.PGSimpleDataSource"); - config.addDataSourceProperty("serverName", address); - config.addDataSourceProperty("portNumber", port); - config.addDataSourceProperty("databaseName", databaseName); - config.addDataSourceProperty("user", username); - config.addDataSourceProperty("password", password); + protected String driverJdbcIdentifier() { + return "postgresql"; } @Override - protected void overrideProperties(Map properties) { + protected void overrideProperties(Map properties) { super.overrideProperties(properties); // remove the default config properties which don't exist for PostgreSQL @@ -67,7 +65,7 @@ protected void overrideProperties(Map properties) { } @Override - public Function getStatementProcessor() { - return s -> s.replace('\'', '"'); + public StatementProcessor getStatementProcessor() { + return StatementProcessor.USE_DOUBLE_QUOTES; } } 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 12a97ca5f..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 @@ -27,7 +27,6 @@ import net.luckperms.api.node.HeldNode; import net.luckperms.api.node.Node; - import org.checkerframework.checker.nullness.qual.NonNull; @SuppressWarnings("deprecation") @@ -55,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/storage/misc/PlayerSaveResultImpl.java b/common/src/main/java/me/lucko/luckperms/common/storage/misc/PlayerSaveResultImpl.java index e8c899d73..b61f5239c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/misc/PlayerSaveResultImpl.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/misc/PlayerSaveResultImpl.java @@ -26,9 +26,7 @@ package me.lucko.luckperms.common.storage.misc; import com.google.common.collect.ImmutableSet; - import net.luckperms.api.model.PlayerSaveResult; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/common/src/main/java/me/lucko/luckperms/common/storage/misc/StorageCredentials.java b/common/src/main/java/me/lucko/luckperms/common/storage/misc/StorageCredentials.java index 731d0d4f6..1545754c3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/storage/misc/StorageCredentials.java +++ b/common/src/main/java/me/lucko/luckperms/common/storage/misc/StorageCredentials.java @@ -25,6 +25,8 @@ package me.lucko.luckperms.common.storage.misc; +import com.google.common.collect.ImmutableMap; + import java.util.Map; import java.util.Objects; @@ -54,6 +56,10 @@ public StorageCredentials(String address, String database, String username, Stri this.properties = properties; } + public StorageCredentials(String address, String database, String username, String password) { + this(address, database, username, password, 10, 10, 1800000, 0, 5000, ImmutableMap.of()); + } + public String getAddress() { return Objects.requireNonNull(this.address, "address"); } diff --git a/common/src/main/java/me/lucko/luckperms/common/tasks/SyncTask.java b/common/src/main/java/me/lucko/luckperms/common/tasks/SyncTask.java index f2c8fb565..28a0aee90 100644 --- a/common/src/main/java/me/lucko/luckperms/common/tasks/SyncTask.java +++ b/common/src/main/java/me/lucko/luckperms/common/tasks/SyncTask.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.cache.BufferedRequest; import me.lucko.luckperms.common.model.manager.group.GroupManager; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.luckperms.api.event.cause.CreationCause; import java.util.concurrent.TimeUnit; diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/AsyncPermissionRegistry.java b/common/src/main/java/me/lucko/luckperms/common/treeview/AsyncPermissionRegistry.java new file mode 100644 index 000000000..68858fc87 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/AsyncPermissionRegistry.java @@ -0,0 +1,70 @@ +/* + * 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.treeview; + +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; + +public class AsyncPermissionRegistry extends PermissionRegistry implements AutoCloseable { + + /** A queue of permission strings to be added to the tree */ + private final Queue queue; + /** The tick task */ + private final SchedulerTask task; + + public AsyncPermissionRegistry(SchedulerAdapter scheduler) { + this.queue = new ConcurrentLinkedQueue<>(); + this.task = scheduler.asyncRepeating(this::tick, 1, TimeUnit.SECONDS); + } + + @Override + public void offer(String permission) { + if (permission == null) { + throw new NullPointerException("permission"); + } + this.queue.offer(permission); + } + + private void tick() { + for (String e; (e = this.queue.poll()) != null; ) { + try { + doInsert(e); + } catch (Exception ex) { + // ignore + } + } + } + + @Override + public void close() { + this.task.cancel(); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/ImmutableTreeNode.java b/common/src/main/java/me/lucko/luckperms/common/treeview/ImmutableTreeNode.java index ba8aeb7c3..725f77c6d 100644 --- a/common/src/main/java/me/lucko/luckperms/common/treeview/ImmutableTreeNode.java +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/ImmutableTreeNode.java @@ -28,7 +28,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.gson.JsonObject; - import org.checkerframework.checker.nullness.qual.NonNull; import java.util.ArrayList; diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/PermissionRegistry.java b/common/src/main/java/me/lucko/luckperms/common/treeview/PermissionRegistry.java index e02c14448..9177bf6f4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/treeview/PermissionRegistry.java +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/PermissionRegistry.java @@ -26,35 +26,20 @@ package me.lucko.luckperms.common.treeview; import com.google.common.base.Splitter; - -import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; -import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; import me.lucko.luckperms.common.util.ImmutableCollectors; import java.util.List; +import java.util.Locale; import java.util.Map; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.TimeUnit; /** * Stores a collection of all permissions known to the platform. */ -public class PermissionRegistry implements AutoCloseable { +public class PermissionRegistry { private static final Splitter DOT_SPLIT = Splitter.on('.').omitEmptyStrings(); /** The root node in the tree */ - private final TreeNode rootNode; - /** A queue of permission strings to be added to the tree */ - private final Queue queue; - /** The tick task */ - private final SchedulerTask task; - - public PermissionRegistry(SchedulerAdapter scheduler) { - this.rootNode = new TreeNode(); - this.queue = new ConcurrentLinkedQueue<>(); - this.task = scheduler.asyncRepeating(this::tick, 1, TimeUnit.SECONDS); - } + private final TreeNode rootNode = new TreeNode(); public TreeNode getRootNode() { return this.rootNode; @@ -66,34 +51,34 @@ public List rootAsList() { .collect(ImmutableCollectors.toList()); } + /** + * Offer a permission to the registry (to be potentially inserted asynchronously). + * + * @param permission the permission + */ public void offer(String permission) { - if (permission == null) { - throw new NullPointerException("permission"); - } - this.queue.offer(permission); + insert(permission); } - private void tick() { - for (String e; (e = this.queue.poll()) != null; ) { - insert(e); + /** + * Insert a permission into the registry. + * + * @param permission the permission + */ + public void insert(String permission) { + if (permission == null) { + throw new NullPointerException("permission"); } - } - @Override - public void close() { - this.task.cancel(); - } - - public void insert(String permission) { try { doInsert(permission); } catch (Exception ex) { - ex.printStackTrace(); + // ignore } } - private void doInsert(String permission) { - permission = permission.toLowerCase(); + protected void doInsert(String permission) { + permission = permission.toLowerCase(Locale.ROOT); // split the permission up into parts Iterable parts = DOT_SPLIT.split(permission); diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeNode.java b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeNode.java index be0b69ce4..6893d18bd 100644 --- a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeNode.java +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeNode.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.treeview; import com.google.common.collect.Maps; - import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Map; diff --git a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeView.java b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeView.java index a015e5920..c24148c2b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/treeview/TreeView.java +++ b/common/src/main/java/me/lucko/luckperms/common/treeview/TreeView.java @@ -27,7 +27,6 @@ import com.google.common.base.Splitter; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.cacheddata.type.PermissionCache; import me.lucko.luckperms.common.http.AbstractHttpClient; import me.lucko.luckperms.common.http.BytebinClient; @@ -36,7 +35,7 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.gson.GsonProvider; import me.lucko.luckperms.common.util.gson.JObject; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -46,6 +45,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.zip.GZIPOutputStream; @@ -148,7 +148,8 @@ public String uploadPasteData(BytebinClient bytebin, Sender sender, User user, P .add("uploader", new JObject() .add("name", sender.getNameWithLocation()) .add("uuid", sender.getUniqueId().toString()) - ); + ) + .add("platform", sender.getPlugin().getBootstrap().getType().getFriendlyName()); JObject checks; if (user != null && checker != null) { @@ -160,7 +161,7 @@ public String uploadPasteData(BytebinClient bytebin, Sender sender, User user, P checks = new JObject(); for (Map.Entry node : this.view.getNodeEndings()) { String permission = prefix + node.getValue(); - checks.add(permission, checker.checkPermission(permission, PermissionCheckEvent.Origin.INTERNAL).result().name().toLowerCase()); + checks.add(permission, checker.checkPermission(permission, CheckOrigin.INTERNAL).result().name().toLowerCase(Locale.ROOT)); } } else { checks = null; diff --git a/common/src/main/java/me/lucko/luckperms/common/util/AsyncInterface.java b/common/src/main/java/me/lucko/luckperms/common/util/AsyncInterface.java new file mode 100644 index 000000000..d1996226e --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/util/AsyncInterface.java @@ -0,0 +1,71 @@ +/* + * 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.util; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +/** + * Base class for an interface which can perform operations asynchronously and return {@link CompletableFuture}s + */ +public abstract class AsyncInterface { + + private final LuckPermsPlugin plugin; + + protected AsyncInterface(LuckPermsPlugin plugin) { + this.plugin = plugin; + } + + protected CompletableFuture future(Callable supplier) { + return CompletableFuture.supplyAsync(() -> { + try { + return supplier.call(); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new CompletionException(e); + } + }, this.plugin.getBootstrap().getScheduler().async()); + } + + protected CompletableFuture future(Throwing.Runnable runnable) { + return CompletableFuture.runAsync(() -> { + try { + runnable.run(); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + throw new CompletionException(e); + } + }, this.plugin.getBootstrap().getScheduler().async()); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/util/CompletableFutures.java b/common/src/main/java/me/lucko/luckperms/common/util/CompletableFutures.java new file mode 100644 index 000000000..93244f82f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/util/CompletableFutures.java @@ -0,0 +1,57 @@ +/* + * 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.util; + +import com.google.common.collect.ImmutableList; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collector; +import java.util.stream.Stream; + +public final class CompletableFutures { + private CompletableFutures() {} + + public static > Collector, CompletableFuture> collector() { + return Collector.of( + ImmutableList.Builder::new, + ImmutableList.Builder::add, + (l, r) -> l.addAll(r.build()), + builder -> allOf(builder.build()) + ); + } + + public static CompletableFuture allOf(Stream> futures) { + CompletableFuture[] arr = futures.toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(arr); + } + + public static CompletableFuture allOf(Collection> futures) { + CompletableFuture[] arr = futures.toArray(new CompletableFuture[0]); + return CompletableFuture.allOf(arr); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/util/Difference.java b/common/src/main/java/me/lucko/luckperms/common/util/Difference.java new file mode 100644 index 000000000..6e30b529a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/util/Difference.java @@ -0,0 +1,201 @@ +/* + * 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.util; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Stream; + +/** + * Records a log of the changes that occur as a result + * of mutations (add or remove operations). + * + * @param the value type + */ +public class Difference { + private final LinkedHashSet> changes = new LinkedHashSet<>(); + + /** + * Gets the recorded changes. + * + * @return the changes + */ + public Set> getChanges() { + return this.changes; + } + + /** + * Gets if no changes have been recorded. + * + * @return if no changes have been recorded + */ + public boolean isEmpty() { + return this.changes.isEmpty(); + } + + /** + * Gets the recorded changes of a given type + * + * @param type the type of change + * @return the changes + */ + public Set getChanges(ChangeType type) { + Set changes = new LinkedHashSet<>(this.changes.size()); + for (Change change : this.changes) { + if (change.type() == type) { + changes.add(change.value()); + } + } + return changes; + } + + /** + * Gets the values that have been added. + * + * @return the added values + */ + public Set getAdded() { + return getChanges(ChangeType.ADD); + } + + /** + * Gets the values that have been removed. + * + * @return the removed values + */ + public Set getRemoved() { + return getChanges(ChangeType.REMOVE); + } + + /** + * Clears all recorded changes. + */ + public void clear() { + this.changes.clear(); + } + + /** + * Records a change. + * + * @param type the type of change + * @param value the changed value + */ + public void recordChange(ChangeType type, T value) { + // This method is the magic of this class. + // When tracking, we want to ignore changes that cancel each other out, and only + // keep track of the net difference. + // e.g. adding then removing the same value = zero net change, so ignore it. + if (this.changes.remove(new Change<>(type.inverse(), value))) { + return; + } + this.changes.add(new Change<>(type, value)); + } + + /** + * Records some changes. + * + * @param type the type of change + * @param values the changed values + */ + public void recordChanges(ChangeType type, Iterable values) { + for (T value : values) { + recordChange(type, value); + } + } + + /** + * Merges the recorded differences in {@code other} into this. + * + * @param other the other differences + * @return this + */ + public Difference mergeFrom(Difference other) { + for (Change change : other.changes) { + recordChange(change.type(), change.value()); + } + return this; + } + + @Override + public String toString() { + return "Difference{" + this.changes + '}'; + } + + /** + * A single change recorded in the {@link Difference} tracker. + * + * @param the value type + */ + public static final class Change { + private final ChangeType type; + private final T value; + private final int hashCode; + + public Change(ChangeType type, T value) { + this.type = type; + this.value = value; + this.hashCode = 31 * type.hashCode() + value.hashCode(); + } + + public ChangeType type() { + return this.type; + } + + public T value() { + return this.value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Change change = (Change) o; + return this.type == change.type && this.value.equals(change.value); + } + + @Override + public int hashCode() { + return this.hashCode; + } + + @Override + public String toString() { + return "(" + this.type + ": " + this.value + ')'; + } + } + + /** + * The type of change. + */ + public enum ChangeType { + ADD, REMOVE; + + public ChangeType inverse() { + return this == ADD ? REMOVE : ADD; + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/util/DurationFormatter.java b/common/src/main/java/me/lucko/luckperms/common/util/DurationFormatter.java index 0e90e881e..126388c1e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/util/DurationFormatter.java +++ b/common/src/main/java/me/lucko/luckperms/common/util/DurationFormatter.java @@ -26,14 +26,14 @@ package me.lucko.luckperms.common.util; import me.lucko.luckperms.common.locale.TranslationManager; - import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TranslatableComponent; -import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import java.time.Duration; import java.time.temporal.ChronoUnit; +import java.util.Locale; /** * Formats durations to a readable form @@ -72,7 +72,7 @@ public DurationFormatter(boolean concise, int accuracy) { * @return the formatted string */ public String formatString(Duration duration) { - return PlainComponentSerializer.plain().serialize(TranslationManager.render(format(duration))); + return PlainTextComponentSerializer.plainText().serialize(TranslationManager.render(format(duration))); } /** @@ -115,7 +115,7 @@ public Component format(Duration duration) { private TranslatableComponent formatPart(long amount, ChronoUnit unit) { String format = this.concise ? "short" : amount == 1 ? "singular" : "plural"; - String translationKey = "luckperms.duration.unit." + unit.name().toLowerCase() + "." + format; + String translationKey = "luckperms.duration.unit." + unit.name().toLowerCase(Locale.ROOT) + "." + format; return Component.translatable(translationKey, Component.text(amount)); } diff --git a/common/src/main/java/me/lucko/luckperms/common/util/EnumNamer.java b/common/src/main/java/me/lucko/luckperms/common/util/EnumNamer.java index 586b62326..13465f6c2 100644 --- a/common/src/main/java/me/lucko/luckperms/common/util/EnumNamer.java +++ b/common/src/main/java/me/lucko/luckperms/common/util/EnumNamer.java @@ -26,6 +26,7 @@ package me.lucko.luckperms.common.util; import java.util.Collections; +import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -35,7 +36,7 @@ * @param the enum type */ public class EnumNamer> { - public static final Function, String> LOWER_CASE_NAME = value -> value.name().toLowerCase(); + public static final Function, String> LOWER_CASE_NAME = value -> value.name().toLowerCase(Locale.ROOT); private final String[] names; private final Function namingFunction; diff --git a/common/src/main/java/me/lucko/luckperms/common/util/ExpiringSet.java b/common/src/main/java/me/lucko/luckperms/common/util/ExpiringSet.java index 70df598ab..d4e12d5c3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/util/ExpiringSet.java +++ b/common/src/main/java/me/lucko/luckperms/common/util/ExpiringSet.java @@ -25,28 +25,21 @@ package me.lucko.luckperms.common.util; -import com.github.benmanes.caffeine.cache.Cache; -import com.google.common.collect.ForwardingSet; - import java.util.Collections; import java.util.Set; import java.util.concurrent.TimeUnit; -/** - * A simple expiring set implementation using Caffeine caches - * - * @param element type - */ -public class ExpiringSet extends ForwardingSet { - private final Set setView; +public final class ExpiringSet { + private ExpiringSet() {} - public ExpiringSet(long duration, TimeUnit unit) { - Cache cache = CaffeineFactory.newBuilder().expireAfterAccess(duration, unit).build(); - this.setView = Collections.newSetFromMap(cache.asMap()); + /** + * An expiring set using Caffeine caches + * + * @param the element type + * @return a new expiring set + */ + public static Set newExpiringSet(long duration, TimeUnit unit) { + return Collections.newSetFromMap(CaffeineFactory.newBuilder().expireAfterWrite(duration, unit).build().asMap()); } - @Override - protected Set delegate() { - return this.setView; - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/util/HostAndPort.java b/common/src/main/java/me/lucko/luckperms/common/util/HostAndPort.java new file mode 100644 index 000000000..b861ce9ab --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/util/HostAndPort.java @@ -0,0 +1,81 @@ +/* + * 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.util; + +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * A wrapper around Guava's HostAndPort to account for the method name change of getHostText(). + */ +@SuppressWarnings("UnstableApiUsage") +public class HostAndPort { + private static final Method GET_HOST_METHOD; + + static { + Method getHostMethod = null; + try { + getHostMethod = com.google.common.net.HostAndPort.class.getMethod("getHostText"); + } catch (NoSuchMethodException e) { + try { + getHostMethod = com.google.common.net.HostAndPort.class.getMethod("getHost"); + } catch (NoSuchMethodException ex) { + // ignore + } + } + Objects.requireNonNull(getHostMethod); + GET_HOST_METHOD = getHostMethod; + } + + private com.google.common.net.HostAndPort delegate; + + public HostAndPort(String hostAndPort) { + this.delegate = com.google.common.net.HostAndPort.fromString(hostAndPort); + } + + public HostAndPort withDefaultPort(int defaultPort) { + this.delegate = this.delegate.withDefaultPort(defaultPort); + return this; + } + + public HostAndPort requireBracketsForIPv6() { + this.delegate.requireBracketsForIPv6(); + return this; + } + + public String getHost() { + try { + return (String) GET_HOST_METHOD.invoke(this.delegate); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + public int getPort() { + return this.delegate.getPort(); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/util/Predicates.java b/common/src/main/java/me/lucko/luckperms/common/util/Predicates.java index 76fdb0137..9f49b7201 100644 --- a/common/src/main/java/me/lucko/luckperms/common/util/Predicates.java +++ b/common/src/main/java/me/lucko/luckperms/common/util/Predicates.java @@ -26,9 +26,9 @@ package me.lucko.luckperms.common.util; import com.google.common.collect.Range; - import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Locale; import java.util.function.Predicate; /** @@ -86,4 +86,22 @@ public static Predicate is(T t) { return t::equals; } + public static Predicate startsWithIgnoreCase(String prefix) { + return string -> { + if (string.length() < prefix.length()) { + return false; + } + return string.regionMatches(true, 0, prefix, 0, prefix.length()); + }; + } + + public static Predicate containsIgnoreCase(String substring) { + return string -> { + if (string.length() < substring.length()) { + return false; + } + return string.toLowerCase(Locale.ROOT).contains(substring.toLowerCase(Locale.ROOT)); + }; + } + } 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 b3dbfdd89..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 @@ -26,9 +26,10 @@ package me.lucko.luckperms.common.util; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; import net.luckperms.api.event.player.lookup.UniqueIdDetermineTypeEvent; import java.util.UUID; @@ -40,16 +41,31 @@ public final class UniqueIdType { public static final UniqueIdType AUTHENTICATED = new UniqueIdType( UniqueIdDetermineTypeEvent.TYPE_AUTHENTICATED, - Component.translatable("luckperms.command.user.info.uuid-type.mojang", NamedTextColor.DARK_GREEN) + NamedTextColor.DARK_GREEN, + "luckperms.command.user.info.uuid-type.mojang", + "luckperms.command.user.info.uuid-type.desc.mojang" ); public static final UniqueIdType UNAUTHENTICATED = new UniqueIdType( UniqueIdDetermineTypeEvent.TYPE_UNAUTHENTICATED, - Component.translatable("luckperms.command.user.info.uuid-type.not-mojang", NamedTextColor.DARK_GRAY) + NamedTextColor.DARK_GRAY, + "luckperms.command.user.info.uuid-type.not-mojang", + "luckperms.command.user.info.uuid-type.desc.not-mojang" + ); + + public static final UniqueIdType NPC = new UniqueIdType( + UniqueIdDetermineTypeEvent.TYPE_NPC, + NamedTextColor.GOLD, + "luckperms.command.user.info.uuid-type.npc", + "luckperms.command.user.info.uuid-type.desc.npc" ); - private static final String TYPE_NPC = "npc"; - public static final UniqueIdType NPC = new UniqueIdType(TYPE_NPC); + public static final UniqueIdType UNKNOWN = new UniqueIdType( + UniqueIdDetermineTypeEvent.TYPE_UNKNOWN, + NamedTextColor.RED, + "luckperms.command.user.info.uuid-type.unknown", + "luckperms.command.user.info.uuid-type.desc.unknown" + ); public static UniqueIdType determineType(UUID uniqueId, LuckPermsPlugin plugin) { // determine initial type based on the uuid version @@ -63,12 +79,12 @@ 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 - type = TYPE_NPC; + // see: https://github.com/LuckPerms/LuckPerms/issues/1470 + // and https://github.com/LuckPerms/LuckPerms/issues/1470#issuecomment-475403162 + type = UniqueIdDetermineTypeEvent.TYPE_NPC; break; default: - type = "unknown"; + type = UniqueIdDetermineTypeEvent.TYPE_UNKNOWN; break; } @@ -80,8 +96,10 @@ public static UniqueIdType determineType(UUID uniqueId, LuckPermsPlugin plugin) return AUTHENTICATED; case UniqueIdDetermineTypeEvent.TYPE_UNAUTHENTICATED: return UNAUTHENTICATED; - case TYPE_NPC: + case UniqueIdDetermineTypeEvent.TYPE_NPC: return NPC; + case UniqueIdDetermineTypeEvent.TYPE_UNKNOWN: + return UNKNOWN; default: return new UniqueIdType(type); } @@ -90,13 +108,30 @@ public static UniqueIdType determineType(UUID uniqueId, LuckPermsPlugin plugin) private final String type; private final Component component; - private UniqueIdType(String type) { - this(type, Component.text(type, NamedTextColor.GOLD)); + // constructor used for built-in types + private UniqueIdType(String type, TextColor displayColor, String translationKey, String translationKeyHover) { + this.type = type; + this.component = Component.translatable() + .key(translationKey) + .color(displayColor) + .hoverEvent(HoverEvent.showText(Component.translatable( + translationKeyHover, + NamedTextColor.DARK_GRAY + ))) + .build(); } - private UniqueIdType(String type, Component component) { + // constructor used for types provided via the API + private UniqueIdType(String type) { this.type = type; - this.component = component; + this.component = Component.text() + .content(type) + .color(NamedTextColor.GOLD) + .hoverEvent(HoverEvent.showText(Component.translatable( + "luckperms.command.user.info.uuid-type.desc.api", + NamedTextColor.GRAY + ))) + .build(); } public String getType() { diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseCheckTarget.java b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseCheckTarget.java index 50d0e82d0..c249c8b3b 100644 --- a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseCheckTarget.java +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseCheckTarget.java @@ -27,9 +27,7 @@ import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.User; - import net.luckperms.api.model.PermissionHolder; - import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Objects; diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseHandler.java b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseHandler.java index 98a09042f..613b802ed 100644 --- a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseHandler.java +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseHandler.java @@ -25,14 +25,15 @@ package me.lucko.luckperms.common.verbose; -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.StringResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; import me.lucko.luckperms.common.verbose.event.VerboseEvent; - import net.luckperms.api.query.QueryOptions; import java.util.Map; @@ -74,7 +75,7 @@ public VerboseHandler(SchedulerAdapter scheduler) { * @param permission the permission which was checked for * @param result the result of the permission check */ - public void offerPermissionCheckEvent(PermissionCheckEvent.Origin origin, VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, String permission, TristateResult result) { + public void offerPermissionCheckEvent(CheckOrigin origin, VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, String permission, TristateResult result) { // don't bother even processing the check if there are no listeners registered if (!this.listening) { return; @@ -100,7 +101,7 @@ public void offerPermissionCheckEvent(PermissionCheckEvent.Origin origin, Verbos * @param key the meta key which was checked for * @param result the result of the meta check */ - public void offerMetaCheckEvent(MetaCheckEvent.Origin origin, VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, String key, String result) { + public void offerMetaCheckEvent(CheckOrigin origin, VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, String key, StringResult result) { // don't bother even processing the check if there are no listeners registered if (!this.listening) { return; diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseListener.java b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseListener.java index 8d942b90b..c5e24e0a4 100644 --- a/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseListener.java +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/VerboseListener.java @@ -26,8 +26,7 @@ package me.lucko.luckperms.common.verbose; import com.google.gson.JsonObject; - -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.http.AbstractHttpClient; import me.lucko.luckperms.common.http.BytebinClient; import me.lucko.luckperms.common.http.UnsuccessfulRequestException; @@ -38,14 +37,17 @@ import me.lucko.luckperms.common.util.gson.GsonProvider; import me.lucko.luckperms.common.util.gson.JArray; import me.lucko.luckperms.common.util.gson.JObject; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; import me.lucko.luckperms.common.verbose.event.VerboseEvent; - import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.event.HoverEvent; -import net.kyori.adventure.text.format.NamedTextColor; +import net.luckperms.api.cacheddata.Result; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.types.MetaNode; import net.luckperms.api.query.QueryMode; import java.io.ByteArrayOutputStream; @@ -71,11 +73,15 @@ public class VerboseListener { .withZone(ZoneId.systemDefault()); // how much data should we store before stopping. - private static final int DATA_TRUNCATION = 10000; + private static final int DATA_TRUNCATION = 10_000; // how many lines should we include in each stack trace send as a chat message private static final int STACK_TRUNCATION_CHAT = 15; // how many lines should we include in each stack trace in the web output private static final int STACK_TRUNCATION_WEB = 40; + // rate limit for notifications: 50 notifications per second for players, 100 for console + private static final int NOTIFICATION_RATE_LIMIT_MAX_EVENTS_PLAYER = 50; + private static final int NOTIFICATION_RATE_LIMIT_MAX_EVENTS_CONSOLE = 100; + private static final long NOTIFICATION_RATE_LIMIT_WINDOW_MS = 1000; private static final StackTracePrinter FILTERING_PRINTER = StackTracePrinter.builder() .ignoreClassStartingWith("me.lucko.luckperms.") @@ -114,6 +120,10 @@ public class VerboseListener { private final AtomicInteger matchedCounter = new AtomicInteger(0); // the events which passed the filter, up to a max size of #DATA_TRUNCATION private final List results = new ArrayList<>(DATA_TRUNCATION / 10); + // a list of timestamps when a notification message was sent, used for rate limiting + private final List notificationTimestamps = new ArrayList<>(); + // track if we've already warned about rate limiting in the current window + private boolean notificationRateLimitWarningShown = false; public VerboseListener(Sender notifiedSender, VerboseFilter filter, boolean notify) { this.notifiedSender = notifiedSender; @@ -150,25 +160,7 @@ public void acceptEvent(VerboseEvent event) { } private void sendNotification(VerboseEvent event) { - if (this.notifiedSender.isConsole()) { - // just send as a raw message - if (event instanceof PermissionCheckEvent) { - PermissionCheckEvent permissionEvent = (PermissionCheckEvent) event; - Message.VERBOSE_LOG_PERMISSION.send(this.notifiedSender, - permissionEvent.getCheckTarget().describe(), - permissionEvent.getPermission(), - permissionEvent.getResult().result() - ); - } else if (event instanceof MetaCheckEvent) { - MetaCheckEvent metaEvent = (MetaCheckEvent) event; - Message.VERBOSE_LOG_META.send(this.notifiedSender, - metaEvent.getCheckTarget().describe(), - metaEvent.getKey(), - metaEvent.getResult() - ); - } else { - throw new IllegalArgumentException("Unknown event type: " + event); - } + if (!checkNotificationRateLimit()) { return; } @@ -186,69 +178,52 @@ private void sendNotification(VerboseEvent event) { component = Message.VERBOSE_LOG_META.build( metaEvent.getCheckTarget().describe(), metaEvent.getKey(), - metaEvent.getResult() + String.valueOf(metaEvent.getResult().result()) ); } else { throw new IllegalArgumentException("Unknown event type: " + event); } + // just send as a raw message + if (this.notifiedSender.isConsole()) { + this.notifiedSender.sendMessage(component); + return; + } + // build the hover text List hover = new ArrayList<>(); - if (event instanceof PermissionCheckEvent) { - PermissionCheckEvent permissionEvent = (PermissionCheckEvent) event; - hover.add(Component.text() - .append(Component.text("Type: ", NamedTextColor.GREEN)) - .append(Component.text("permission", NamedTextColor.DARK_GREEN)) - ); - hover.add(Component.text() - .append(Component.text("Origin: ", NamedTextColor.AQUA)) - .append(Component.text(permissionEvent.getOrigin().name(), NamedTextColor.DARK_GREEN)) - ); + hover.add(Message.VERBOSE_LOG_HOVER_TYPE.build(event.getType().toString())); + hover.add(Message.VERBOSE_LOG_HOVER_ORIGIN.build(event.getOrigin().name())); - TristateResult result = permissionEvent.getResult(); - if (result.processorClass() != null) { - hover.add(Component.text() - .append(Component.text("Processor: ", NamedTextColor.AQUA)) - .append(Component.text(result.processorClass().getName(), NamedTextColor.DARK_GREEN)) - ); - } - if (result.cause() != null) { - hover.add(Component.text() - .append(Component.text("Cause: ", NamedTextColor.AQUA)) - .append(Component.text(result.cause(), NamedTextColor.DARK_GREEN)) - ); + Result result = event.getResult(); + + if (result instanceof TristateResult) { + TristateResult tristateResult = (TristateResult) result; + + if (tristateResult.processorClass() != null) { + hover.add(Message.VERBOSE_LOG_HOVER_PROCESSOR.build(tristateResult.processorClassFriendly())); } } - if (event instanceof MetaCheckEvent) { - MetaCheckEvent metaEvent = (MetaCheckEvent) event; - hover.add(Component.text() - .append(Component.text("Type: ", NamedTextColor.GREEN)) - .append(Component.text("meta", NamedTextColor.DARK_GREEN)) - ); - hover.add(Component.text() - .append(Component.text("Origin: ", NamedTextColor.AQUA)) - .append(Component.text(metaEvent.getOrigin().name(), NamedTextColor.DARK_GREEN)) - ); + + Node node = result.node(); + if (node != null) { + if (node instanceof MetaNode) { + hover.add(Message.VERBOSE_LOG_HOVER_CAUSE_META.build((MetaNode) node)); + } else { + hover.add(Message.VERBOSE_LOG_HOVER_CAUSE.build(node)); + } } if (event.getCheckQueryOptions().mode() == QueryMode.CONTEXTUAL) { - hover.add(Component.text() - .append(Component.text("Context: ", NamedTextColor.AQUA)) - .append(Message.formatContextSet(event.getCheckQueryOptions().context())) - ); + hover.add(Message.VERBOSE_LOG_HOVER_CONTEXT.build(event.getCheckQueryOptions().context())); } - hover.add(Component.text() - .append(Component.text("Thread: ", NamedTextColor.AQUA)) - .append(Component.text(event.getCheckThread(), NamedTextColor.WHITE)) - ); + hover.add(Message.VERBOSE_LOG_HOVER_THREAD.build(event.getCheckThread())); - hover.add(Component.text() - .append(Component.text("Trace: ", NamedTextColor.AQUA)) - ); + hover.add(Message.VERBOSE_LOG_HOVER_TRACE_TITLE.build()); - Consumer printer = StackTracePrinter.elementToString(str -> hover.add(Component.text(str, NamedTextColor.GRAY))); + Consumer printer = StackTracePrinter.elementToString(str -> hover.add(Message.VERBOSE_LOG_HOVER_TRACE_CONTENT.build(str))); int overflow; if (shouldFilterStackTrace(event)) { overflow = CHAT_FILTERED_PRINTER.process(event.getCheckTrace(), printer); @@ -256,19 +231,49 @@ private void sendNotification(VerboseEvent event) { overflow = CHAT_UNFILTERED_PRINTER.process(event.getCheckTrace(), printer); } if (overflow != 0) { - hover.add(Component.text("... and " + overflow + " more", NamedTextColor.WHITE)); + hover.add(Message.VERBOSE_LOG_HOVER_TRACE_OVERFLOW.build(overflow)); } // send the message - HoverEvent hoverEvent = HoverEvent.showText(Component.join(Component.newline(), hover)); + HoverEvent hoverEvent = HoverEvent.showText(Component.join(JoinConfiguration.newlines(), hover)); this.notifiedSender.sendMessage(component.hoverEvent(hoverEvent)); } + /** + * Check if we should send a notification based on rate limiting. + * + * @return true if the notification should be sent, false if rate limited + */ + private boolean checkNotificationRateLimit() { + long now = System.currentTimeMillis(); + int maxEvents = this.notifiedSender.isConsole() ? NOTIFICATION_RATE_LIMIT_MAX_EVENTS_CONSOLE : NOTIFICATION_RATE_LIMIT_MAX_EVENTS_PLAYER; + + // remove timestamps outside the current window + this.notificationTimestamps.removeIf(timestamp -> (now - timestamp) > NOTIFICATION_RATE_LIMIT_WINDOW_MS); + + if (this.notificationTimestamps.size() >= maxEvents) { + if (!this.notificationRateLimitWarningShown) { + Message.VERBOSE_NOTIFICATION_RATE_LIMITED.send(this.notifiedSender); + this.notificationRateLimitWarningShown = true; + } + return false; + } + + this.notificationTimestamps.add(now); + + // reset the warning flag if we're back under the limit + if (this.notificationTimestamps.size() < maxEvents) { + this.notificationRateLimitWarningShown = false; + } + + return true; + } + private static boolean shouldFilterStackTrace(VerboseEvent event) { if (event instanceof PermissionCheckEvent) { PermissionCheckEvent permissionEvent = (PermissionCheckEvent) event; - return permissionEvent.getOrigin() == PermissionCheckEvent.Origin.PLATFORM_LOOKUP_CHECK || - permissionEvent.getOrigin() == PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK; + return permissionEvent.getOrigin() == CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET || + permissionEvent.getOrigin() == CheckOrigin.PLATFORM_API_HAS_PERMISSION; } return false; } @@ -299,11 +304,12 @@ public String uploadPasteData(BytebinClient bytebin) throws IOException, Unsucce .add("uuid", this.notifiedSender.getUniqueId().toString()) ) .add("filter", this.filter.toString()) - .add("truncated", truncated); + .add("truncated", truncated) + .add("platform", this.notifiedSender.getPlugin().getBootstrap().getType().getFriendlyName()); JArray data = new JArray(); - for (VerboseEvent events : this.results) { - data.add(events.toJson(shouldFilterStackTrace(events) ? WEB_FILTERED_PRINTER : WEB_UNFILTERED_PRINTER)); + for (VerboseEvent event : this.results) { + data.add(event.toJson(shouldFilterStackTrace(event) ? WEB_FILTERED_PRINTER : WEB_UNFILTERED_PRINTER)); } this.results.clear(); diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/event/CheckOrigin.java b/common/src/main/java/me/lucko/luckperms/common/verbose/event/CheckOrigin.java new file mode 100644 index 000000000..6064f0b00 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/event/CheckOrigin.java @@ -0,0 +1,63 @@ +/* + * 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.verbose.event; + +/** + * Represents the origin of a meta check + */ +public enum CheckOrigin { + + /** + * Indicates the check was caused by a lookup in a platform API + */ + PLATFORM_API, + + /** + * Indicates the check was caused by a 'hasPermission' check on the platform + */ + PLATFORM_API_HAS_PERMISSION, + + /** + * Indicates the check was caused by a 'hasPermissionSet' type check on the platform + */ + PLATFORM_API_HAS_PERMISSION_SET, + + /** + * Indicates the check was caused by a 3rd party API call + */ + THIRD_PARTY_API, + + /** + * Indicates the check was caused by a LuckPerms API call + */ + LUCKPERMS_API, + + /** + * Indicates the check was caused by a LuckPerms internal + */ + INTERNAL + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/event/MetaCheckEvent.java b/common/src/main/java/me/lucko/luckperms/common/verbose/event/MetaCheckEvent.java index 6c74b70ae..20169c0cf 100644 --- a/common/src/main/java/me/lucko/luckperms/common/verbose/event/MetaCheckEvent.java +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/event/MetaCheckEvent.java @@ -25,17 +25,16 @@ package me.lucko.luckperms.common.verbose.event; +import com.google.gson.JsonArray; +import me.lucko.luckperms.common.cacheddata.result.StringResult; +import me.lucko.luckperms.common.node.utils.NodeJsonSerializer; import me.lucko.luckperms.common.util.gson.JObject; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; - import net.luckperms.api.query.QueryOptions; -public class MetaCheckEvent extends VerboseEvent { +import java.util.Locale; - /** - * The origin of the check - */ - private final Origin origin; +public class MetaCheckEvent extends VerboseEvent { /** * The meta key which was checked for @@ -45,67 +44,67 @@ public class MetaCheckEvent extends VerboseEvent { /** * The result of the meta check */ - private final String result; + private final StringResult result; - public MetaCheckEvent(Origin origin, VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, long checkTime, Throwable checkTrace, String checkThread, String key, String result) { - super(checkTarget, checkQueryOptions, checkTime, checkTrace, checkThread); - this.origin = origin; + public MetaCheckEvent(CheckOrigin origin, VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, long checkTime, Throwable checkTrace, String checkThread, String key, StringResult result) { + super(origin, checkTarget, checkQueryOptions, checkTime, checkTrace, checkThread); this.key = key; this.result = result; } - public Origin getOrigin() { - return this.origin; - } - public String getKey() { return this.key; } - public String getResult() { + @Override + public StringResult getResult() { return this.result; } @Override - protected void serializeTo(JObject object) { - object.add("type", "meta") - .add("key", this.key) - .add("result", this.result) - .add("origin", this.origin.name().toLowerCase()); + public VerboseEventType getType() { + return VerboseEventType.META; } @Override - public boolean eval(String variable) { - return variable.equals("meta") || - getCheckTarget().describe().equalsIgnoreCase(variable) || - getKey().toLowerCase().startsWith(variable.toLowerCase()) || - getResult().equalsIgnoreCase(variable); + protected void serializeTo(JObject object) { + object.add("key", this.key); + + object.add("result", String.valueOf(this.result.result())); + if (this.result != StringResult.nullResult()) { + object.add("resultInfo", serializeResult(this.result)); + } } - /** - * Represents the origin of a meta check - */ - public enum Origin { + private static JObject serializeResult(StringResult result) { + JObject object = new JObject(); + object.add("result", String.valueOf(result.result())); - /** - * Indicates the check was caused by a lookup in a platform API - */ - PLATFORM_API, + if (result.node() != null) { + object.add("node", NodeJsonSerializer.serializeNode(result.node(), true)); + } - /** - * Indicates the check was caused by a 3rd party API call - */ - THIRD_PARTY_API, + if (result.overriddenResult() != null) { + JsonArray overridden = new JsonArray(); - /** - * Indicates the check was caused by a LuckPerms API call - */ - LUCKPERMS_API, + StringResult next = result.overriddenResult(); + while (next != null) { + overridden.add(serializeResult(next).toJson()); + next = next.overriddenResult(); + } - /** - * Indicates the check was caused by a LuckPerms internal - */ - INTERNAL + object.add("overridden", overridden); + } + return object; } + + @Override + public boolean eval(String variable) { + return variable.equals("meta") || + getCheckTarget().describe().equalsIgnoreCase(variable) || + getKey().toLowerCase(Locale.ROOT).startsWith(variable.toLowerCase(Locale.ROOT)) || + String.valueOf(getResult().result()).equalsIgnoreCase(variable); + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/event/PermissionCheckEvent.java b/common/src/main/java/me/lucko/luckperms/common/verbose/event/PermissionCheckEvent.java index 48b200ab9..f0a06270e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/verbose/event/PermissionCheckEvent.java +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/event/PermissionCheckEvent.java @@ -25,18 +25,16 @@ package me.lucko.luckperms.common.verbose.event; -import me.lucko.luckperms.common.calculator.result.TristateResult; +import com.google.gson.JsonArray; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.node.utils.NodeJsonSerializer; import me.lucko.luckperms.common.util.gson.JObject; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; - import net.luckperms.api.query.QueryOptions; -public class PermissionCheckEvent extends VerboseEvent { +import java.util.Locale; - /** - * The origin of the check - */ - private final Origin origin; +public class PermissionCheckEvent extends VerboseEvent { /** * The permission which was checked for @@ -48,82 +46,68 @@ public class PermissionCheckEvent extends VerboseEvent { */ private final TristateResult result; - public PermissionCheckEvent(Origin origin, VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, long checkTime, Throwable checkTrace, String checkThread, String permission, TristateResult result) { - super(checkTarget, checkQueryOptions, checkTime, checkTrace, checkThread); - this.origin = origin; + public PermissionCheckEvent(CheckOrigin origin, VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, long checkTime, Throwable checkTrace, String checkThread, String permission, TristateResult result) { + super(origin, checkTarget, checkQueryOptions, checkTime, checkTrace, checkThread); this.permission = permission; this.result = result; } - public Origin getOrigin() { - return this.origin; - } - public String getPermission() { return this.permission; } + @Override public TristateResult getResult() { return this.result; } + @Override + public VerboseEventType getType() { + return VerboseEventType.PERMISSION; + } + @Override protected void serializeTo(JObject object) { - object.add("type", "permission"); object.add("permission", this.permission); - object.add("result", this.result.result().name().toLowerCase()); - if (this.result.processorClass() != null || this.result.cause() != null) { - JObject resultInfo = new JObject(); - if (this.result.processorClass() != null) { - resultInfo.add("processorClass", this.result.processorClass().getName()); - } - if (this.result.cause() != null) { - resultInfo.add("cause", this.result.cause()); + object.add("result", this.result.result().name().toLowerCase(Locale.ROOT)); + if (this.result != TristateResult.UNDEFINED) { + object.add("resultInfo", serializeResult(this.result)); + } + } + + private static JObject serializeResult(TristateResult result) { + JObject object = new JObject(); + object.add("result", result.result().name().toLowerCase(Locale.ROOT)); + + if (result.processorClass() != null) { + object.add("processorClass", result.processorClass().getName()); + } + if (result.node() != null) { + object.add("node", NodeJsonSerializer.serializeNode(result.node(), true)); + } + + if (result.overriddenResult() != null) { + JsonArray overridden = new JsonArray(); + + TristateResult next = result.overriddenResult(); + while (next != null) { + overridden.add(serializeResult(next).toJson()); + next = next.overriddenResult(); } - object.add("resultInfo", resultInfo); + + object.add("overridden", overridden); } - object.add("origin", this.origin.name().toLowerCase()); + return object; } @Override public boolean eval(String variable) { return variable.equals("permission") || getCheckTarget().describe().equalsIgnoreCase(variable) || - getPermission().toLowerCase().startsWith(variable.toLowerCase()) || + getPermission().toLowerCase(Locale.ROOT).startsWith(variable.toLowerCase(Locale.ROOT)) || getResult().result().name().equalsIgnoreCase(variable); } - /** - * Represents the origin of a permission check - */ - public enum Origin { - - /** - * Indicates the check was caused by a 'hasPermission' check on the platform - */ - PLATFORM_PERMISSION_CHECK, - - /** - * Indicates the check was caused by a 'hasPermissionSet' type check on the platform - */ - PLATFORM_LOOKUP_CHECK, - - /** - * Indicates the check was caused by a 3rd party API call - */ - THIRD_PARTY_API, - - /** - * Indicates the check was caused by an LuckPerms API call - */ - LUCKPERMS_API, - - /** - * Indicates the check was caused by a LuckPerms internal - */ - INTERNAL - - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/event/VerboseEvent.java b/common/src/main/java/me/lucko/luckperms/common/verbose/event/VerboseEvent.java index aff8d66a8..2f4f39188 100644 --- a/common/src/main/java/me/lucko/luckperms/common/verbose/event/VerboseEvent.java +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/event/VerboseEvent.java @@ -26,17 +26,17 @@ package me.lucko.luckperms.common.verbose.event; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.util.StackTracePrinter; import me.lucko.luckperms.common.util.gson.JArray; import me.lucko.luckperms.common.util.gson.JObject; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler.VariableEvaluator; - +import net.luckperms.api.cacheddata.Result; import net.luckperms.api.context.Context; import net.luckperms.api.query.QueryMode; import net.luckperms.api.query.QueryOptions; +import java.util.Locale; import java.util.Objects; import java.util.UUID; @@ -45,6 +45,11 @@ */ public abstract class VerboseEvent implements VariableEvaluator { + /** + * The origin of the check + */ + private final CheckOrigin origin; + /** * The name of the entity which was checked */ @@ -70,7 +75,8 @@ public abstract class VerboseEvent implements VariableEvaluator { */ private final String checkThread; - protected VerboseEvent(VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, long checkTime, Throwable checkTrace, String checkThread) { + protected VerboseEvent(CheckOrigin origin, VerboseCheckTarget checkTarget, QueryOptions checkQueryOptions, long checkTime, Throwable checkTrace, String checkThread) { + this.origin = origin; this.checkTarget = checkTarget; this.checkQueryOptions = checkQueryOptions; this.checkTime = checkTime; @@ -78,10 +84,16 @@ protected VerboseEvent(VerboseCheckTarget checkTarget, QueryOptions checkQueryOp this.checkThread = checkThread; } + public CheckOrigin getOrigin() { + return this.origin; + } + public VerboseCheckTarget getCheckTarget() { return this.checkTarget; } + public abstract Result getResult(); + public QueryOptions getCheckQueryOptions() { return this.checkQueryOptions; } @@ -98,10 +110,14 @@ public String getCheckThread() { return this.checkThread; } + public abstract VerboseEventType getType(); + protected abstract void serializeTo(JObject object); public JsonObject toJson(StackTracePrinter tracePrinter) { return new JObject() + .add("type", getType().toString()) + .add("origin", this.origin.name().toLowerCase(Locale.ROOT)) .add("who", new JObject() .add("identifier", this.checkTarget.describe()) .add("type", this.checkTarget.getType()) @@ -113,7 +129,7 @@ public JsonObject toJson(StackTracePrinter tracePrinter) { } }) ) - .add("queryMode", this.checkQueryOptions.mode().name().toLowerCase()) + .add("queryMode", this.checkQueryOptions.mode().name().toLowerCase(Locale.ROOT)) .consume(obj -> { if (this.checkQueryOptions.mode() == QueryMode.CONTEXTUAL) { obj.add("context", new JArray() diff --git a/common/src/main/java/me/lucko/luckperms/common/verbose/event/VerboseEventType.java b/common/src/main/java/me/lucko/luckperms/common/verbose/event/VerboseEventType.java new file mode 100644 index 000000000..367262962 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/verbose/event/VerboseEventType.java @@ -0,0 +1,46 @@ +/* + * 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.verbose.event; + +import java.util.Locale; + +public enum VerboseEventType { + + /** + * {@link PermissionCheckEvent} + */ + PERMISSION, + + /** + * {@link MetaCheckEvent} + */ + META; + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorRequest.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorRequest.java index ca286e34a..191cae624 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorRequest.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorRequest.java @@ -27,15 +27,11 @@ import com.google.common.base.Preconditions; import com.google.gson.JsonObject; - -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.ContextSetJsonSerializer; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; -import me.lucko.luckperms.common.http.AbstractHttpClient; -import me.lucko.luckperms.common.http.UnsuccessfulRequestException; -import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.serializer.ContextSetJsonSerializer; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.PermissionHolder; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.node.matcher.ConstraintNodeMatcher; @@ -43,11 +39,11 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.storage.misc.NodeEntry; +import me.lucko.luckperms.common.util.ImmutableCollectors; import me.lucko.luckperms.common.util.gson.GsonProvider; import me.lucko.luckperms.common.util.gson.JArray; import me.lucko.luckperms.common.util.gson.JObject; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; @@ -63,8 +59,11 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.GZIPOutputStream; @@ -75,6 +74,48 @@ public class WebEditorRequest { public static final int MAX_USERS = 500; + /** + * The encoded json object this payload is made up of + */ + private final JsonObject payload; + + private final Map> holders; + private final Map> tracks; + + private WebEditorRequest(JsonObject payload, Map> holders, Map> tracks) { + this.payload = payload; + this.holders = holders.entrySet().stream().collect(ImmutableCollectors.toMap( + e -> e.getKey().getIdentifier(), + Map.Entry::getValue + )); + this.tracks = tracks.entrySet().stream().collect(ImmutableCollectors.toMap( + e -> e.getKey().getName(), + Map.Entry::getValue + )); + } + + public JsonObject getPayload() { + return this.payload; + } + + public byte[] encode() { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + try (Writer writer = new OutputStreamWriter(new GZIPOutputStream(bytesOut), StandardCharsets.UTF_8)) { + GsonProvider.normal().toJson(this.payload, writer); + } catch (IOException e) { + e.printStackTrace(); + } + return bytesOut.toByteArray(); + } + + public Map> getHolders() { + return this.holders; + } + + public Map> getTracks() { + return this.tracks; + } + /** * Generates a web editor request payload. * @@ -95,37 +136,38 @@ public static WebEditorRequest generate(List holders, List> holdersMap = holders.stream().collect(ImmutableCollectors.toMap( + Function.identity(), + holder -> holder.normalData().asList() + )); - /** - * The encoded json object this payload is made up of - */ - private final JsonObject payload; + Map> tracksMap = tracks.stream().collect(ImmutableCollectors.toMap( + Function.identity(), + Track::getGroups + )); - private WebEditorRequest(List holders, List tracks, Sender sender, String cmdLabel, ImmutableContextSet potentialContexts, LuckPermsPlugin plugin) { - this.payload = new JObject() - .add("metadata", formMetadata(sender, cmdLabel, plugin.getBootstrap().getVersion())) - .add("permissionHolders", new JArray() - .consume(arr -> { - for (PermissionHolder holder : holders) { - arr.add(formPermissionHolder(holder)); - } - }) - ) - .add("tracks", new JArray() - .consume(arr -> { - for (Track track : tracks) { - arr.add(formTrack(track)); - } - }) - ) + JsonObject json = createJsonPayload(holdersMap, tracksMap, sender, cmdLabel, potentialContexts.build(), plugin).toJson(); + return new WebEditorRequest(json, holdersMap, tracksMap); + } + + private static JObject createJsonPayload(Map> holders, Map> tracks, Sender sender, String cmdLabel, ImmutableContextSet potentialContexts, LuckPermsPlugin plugin) { + return new JObject() + .add("metadata", formMetadata(sender, cmdLabel, plugin.getBootstrap().getVersion(), plugin.getBootstrap().getType().getFriendlyName())) + .add("permissionHolders", new JArray().consume(arr -> + holders.forEach((holder, data) -> + arr.add(formPermissionHolder(holder, data)) + ) + )) + .add("tracks", new JArray().consume(arr -> + tracks.forEach((track, data) -> + arr.add(formTrack(track, data)) + ) + )) .add("knownPermissions", new JArray().addAll(plugin.getPermissionRegistry().rootAsList())) - .add("potentialContexts", ContextSetJsonSerializer.serialize(potentialContexts)) - .toJson(); + .add("potentialContexts", ContextSetJsonSerializer.serialize(potentialContexts)); } - private static JObject formMetadata(Sender sender, String cmdLabel, String pluginVersion) { + private static JObject formMetadata(Sender sender, String cmdLabel, String pluginVersion, String platform) { return new JObject() .add("commandAlias", cmdLabel) .add("uploader", new JObject() @@ -133,57 +175,23 @@ private static JObject formMetadata(Sender sender, String cmdLabel, String plugi .add("uuid", sender.getUniqueId().toString()) ) .add("time", System.currentTimeMillis()) - .add("pluginVersion", pluginVersion); + .add("pluginVersion", pluginVersion) + .add("platform", platform); } - private static JObject formPermissionHolder(PermissionHolder holder) { + private static JObject formPermissionHolder(PermissionHolder holder, List data) { return new JObject() .add("type", holder.getType().toString()) - .add("id", holder.getObjectName()) + .add("id", holder.getIdentifier().getName()) .add("displayName", holder.getPlainDisplayName()) - .add("nodes", NodeJsonSerializer.serializeNodes(holder.normalData().asList())); + .add("nodes", NodeJsonSerializer.serializeNodes(data)); } - private static JObject formTrack(Track track) { + private static JObject formTrack(Track track, List data) { return new JObject() .add("type", "track") .add("id", track.getName()) - .add("groups", new JArray().addAll(track.getGroups())); - } - - public byte[] encode() { - ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - try (Writer writer = new OutputStreamWriter(new GZIPOutputStream(bytesOut), StandardCharsets.UTF_8)) { - GsonProvider.normal().toJson(this.payload, writer); - } catch (IOException e) { - e.printStackTrace(); - } - return bytesOut.toByteArray(); - } - - /** - * Creates a web editor session, and sends the URL to the sender. - * - * @param plugin the plugin - * @param sender the sender creating the session - * @return the command result - */ - public void createSession(LuckPermsPlugin plugin, Sender sender) { - String pasteId; - try { - pasteId = plugin.getBytebin().postContent(encode(), AbstractHttpClient.JSON_TYPE).key(); - } catch (UnsuccessfulRequestException e) { - Message.EDITOR_HTTP_REQUEST_FAILURE.send(sender, e.getResponse().code(), e.getResponse().message()); - return; - } catch (IOException e) { - new RuntimeException("Error uploading data to bytebin", e).printStackTrace(); - Message.EDITOR_HTTP_UNKNOWN_FAILURE.send(sender); - return; - } - - // form a url for the editor - String url = plugin.getConfiguration().get(ConfigKeys.WEB_EDITOR_URL_PATTERN) + pasteId; - Message.EDITOR_URL.send(sender, url); + .add("groups", new JArray().addAll(data)); } public static void includeMatchingGroups(List holders, Predicate filter, LuckPermsPlugin plugin) { @@ -231,7 +239,7 @@ public static void includeMatchingUsers(List holders, CollectioncomparingInt(u -> u.getCachedData().getMetaData(QueryOptions.nonContextual()).getWeight(MetaCheckEvent.Origin.INTERNAL)).reversed() + .comparingInt(u -> u.getCachedData().getMetaData(QueryOptions.nonContextual()).getWeight(CheckOrigin.INTERNAL).intResult()).reversed() // then, prioritise users we actually have a username for .thenComparing(u -> u.getUsername().isPresent(), ((Comparator) Boolean::compare).reversed()) // then sort according to their username @@ -250,17 +258,24 @@ private static void findMatchingOfflineUsers(Map users, ConstraintNo .distinct(); } - stream.filter(uuid -> !users.containsKey(uuid)) + Set uuids = stream + .filter(uuid -> !users.containsKey(uuid)) .sorted() .limit(MAX_USERS - users.size()) - .map(uuid -> plugin.getStorage().loadUser(uuid, null)) - .forEach(fut -> { - User user = fut.join(); - if (user != null) { - users.put(user.getUniqueId(), user); - plugin.getUserManager().getHouseKeeper().cleanup(user.getUniqueId()); - } - }); + .collect(Collectors.toSet()); + + if (uuids.isEmpty()) { + return; + } + + // load users in bulk from storage + Map loadedUsers = plugin.getStorage().loadUsers(uuids).join(); + users.putAll(loadedUsers); + + // schedule cleanup + for (UUID uniqueId : loadedUsers.keySet()) { + plugin.getUserManager().getHouseKeeper().cleanup(uniqueId); + } } } diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorResponse.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorResponse.java index 54af73721..a4d5eeb8e 100644 --- a/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorResponse.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorResponse.java @@ -28,24 +28,23 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.actionlog.LoggedAction; import me.lucko.luckperms.common.command.access.ArgumentPermissions; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.utils.StorageAssistant; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.PermissionHolder; import me.lucko.luckperms.common.model.Track; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.group.GroupManager; -import me.lucko.luckperms.common.model.nodemap.MutateResult; import me.lucko.luckperms.common.node.utils.NodeJsonSerializer; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.Difference; import me.lucko.luckperms.common.util.Uuids; - +import me.lucko.luckperms.common.webeditor.store.RemoteSession; import net.kyori.adventure.text.Component; import net.luckperms.api.actionlog.Action; import net.luckperms.api.event.cause.CreationCause; @@ -55,7 +54,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.UUID; @@ -65,12 +63,18 @@ */ public class WebEditorResponse { + /** + * The id of the response payload + */ + private final String id; + /** * The encoded json object this payload is made up of */ private final JsonObject payload; - public WebEditorResponse(JsonObject payload) { + public WebEditorResponse(String id, JsonObject payload) { + this.id = id; this.payload = payload; } @@ -80,14 +84,31 @@ public WebEditorResponse(JsonObject payload) { * @param plugin the plugin * @param sender the sender who is applying the session */ - public void apply(LuckPermsPlugin plugin, Sender sender) { - Session session = new Session(plugin, sender); + public void apply(LuckPermsPlugin plugin, Sender sender, WebEditorSession editorSession, String commandLabel, boolean ignoreSessionWarning) { + String sessionId = this.payload.get("sessionId").getAsString(); + RemoteSession remoteSession = plugin.getWebEditorStore().sessions().getSession(sessionId); + + if (remoteSession == null) { + // session is unknown + if (!ignoreSessionWarning) { + Message.APPLY_EDITS_SESSION_UNKNOWN.send(sender, this.id, commandLabel); + return; + } + } else if (remoteSession.isCompleted()) { + // session has been completed already + if (!ignoreSessionWarning) { + Message.APPLY_EDITS_SESSION_APPLIED_ALREADY.send(sender, this.id, commandLabel); + return; + } + } + + ChangeApplier changeApplier = new ChangeApplier(plugin, sender, editorSession, remoteSession); boolean work = false; if (this.payload.has("changes")) { JsonArray changes = this.payload.get("changes").getAsJsonArray(); for (JsonElement change : changes) { - if (session.applyChange(change.getAsJsonObject())) { + if (changeApplier.applyChange(change.getAsJsonObject())) { work = true; } } @@ -95,7 +116,7 @@ public void apply(LuckPermsPlugin plugin, Sender sender) { if (this.payload.has("userDeletions")) { JsonArray userDeletions = this.payload.get("userDeletions").getAsJsonArray(); for (JsonElement userDeletion : userDeletions) { - if (session.applyUserDelete(userDeletion)) { + if (changeApplier.applyUserDelete(userDeletion)) { work = true; } } @@ -103,7 +124,7 @@ public void apply(LuckPermsPlugin plugin, Sender sender) { if (this.payload.has("groupDeletions")) { JsonArray groupDeletions = this.payload.get("groupDeletions").getAsJsonArray(); for (JsonElement groupDeletion : groupDeletions) { - if (session.applyGroupDelete(groupDeletion)) { + if (changeApplier.applyGroupDelete(groupDeletion)) { work = true; } } @@ -111,12 +132,16 @@ public void apply(LuckPermsPlugin plugin, Sender sender) { if (this.payload.has("trackDeletions")) { JsonArray trackDeletions = this.payload.get("trackDeletions").getAsJsonArray(); for (JsonElement trackDeletion : trackDeletions) { - if (session.applyTrackDelete(trackDeletion)) { + if (changeApplier.applyTrackDelete(trackDeletion)) { work = true; } } } + if (remoteSession != null) { + remoteSession.complete(); + } + if (!work) { Message.APPLY_EDITS_TARGET_NO_CHANGES_PRESENT.send(sender); } @@ -125,13 +150,17 @@ public void apply(LuckPermsPlugin plugin, Sender sender) { /** * Represents the application of a given editor session on this platform. */ - private static class Session { + private static class ChangeApplier { private final LuckPermsPlugin plugin; private final Sender sender; + private final WebEditorSession session; + private final RemoteSession remoteSession; - Session(LuckPermsPlugin plugin, Sender sender) { + ChangeApplier(LuckPermsPlugin plugin, Sender sender, WebEditorSession session, RemoteSession remoteSession) { this.plugin = plugin; this.sender = sender; + this.session = session; + this.remoteSession = remoteSession; } private boolean applyChange(JsonObject changeInfo) { @@ -169,6 +198,9 @@ private boolean applyHolderChange(JsonObject changeInfo) { holder = this.plugin.getStorage().loadGroup(id).join().orElse(null); if (holder == null) { holder = this.plugin.getStorage().createAndLoadGroup(id, CreationCause.WEB_EDITOR).join(); + if (this.session != null) { + this.session.includeCreatedGroup((Group) holder); + } } } @@ -178,7 +210,7 @@ private boolean applyHolderChange(JsonObject changeInfo) { } Set nodes = NodeJsonSerializer.deserializeNodes(changeInfo.getAsJsonArray("nodes")); - MutateResult res = holder.setNodes(DataType.NORMAL, nodes, true); + Difference res = applyNodeChanges(holder, nodes); if (res.isEmpty()) { return false; @@ -200,22 +232,51 @@ private boolean applyHolderChange(JsonObject changeInfo) { Message.APPLY_EDITS_SUCCESS.send(this.sender, type, holder.getFormattedDisplayName()); Message.APPLY_EDITS_SUCCESS_SUMMARY.send(this.sender, added.size(), removed.size()); + for (Node n : added) { Message.APPLY_EDITS_DIFF_ADDED.send(this.sender, n); } for (Node n : removed) { Message.APPLY_EDITS_DIFF_REMOVED.send(this.sender, n); } + StorageAssistant.save(holder, this.sender, this.plugin); return true; } + private Difference applyNodeChanges(PermissionHolder holder, Set nodes) { + if (this.remoteSession != null) { + + WebEditorRequest request = this.remoteSession.request(); + if (request != null) { + + List nodesBefore = request.getHolders().get(holder.getIdentifier()); + if (nodesBefore != null) { + + // if the initial data sent to the remote session is still known + // use that to calculate a diff of the changes made to avoid overriding + // modified/added/removed nodes since the editor session was created + Difference diff = new Difference<>(); + diff.recordChanges(Difference.ChangeType.REMOVE, nodesBefore); + diff.recordChanges(Difference.ChangeType.ADD, nodes); + + return holder.setNodes(DataType.NORMAL, diff, true); + } + } + } + + return holder.setNodes(DataType.NORMAL, nodes, true); + } + private boolean applyTrackChange(JsonObject changeInfo) { String id = changeInfo.get("id").getAsString(); Track track = this.plugin.getStorage().loadTrack(id).join().orElse(null); if (track == null) { track = this.plugin.getStorage().createAndLoadTrack(id, CreationCause.WEB_EDITOR).join(); + if (this.session != null) { + this.session.includeCreatedTrack(track); + } } if (ArgumentPermissions.checkModifyPerms(this.plugin, this.sender, CommandPermission.APPLY_EDITS, track)) { @@ -231,32 +292,33 @@ private boolean applyTrackChange(JsonObject changeInfo) { return false; } - Set diffAdded = getAdded(before, after); - Set diffRemoved = getRemoved(before, after); + Difference diff = new Difference<>(); + diff.recordChanges(Difference.ChangeType.REMOVE, before); + diff.recordChanges(Difference.ChangeType.ADD, after); - int additions = diffAdded.size(); - int deletions = diffRemoved.size(); + Set added = diff.getAdded(); + Set removed = diff.getRemoved(); track.setGroups(after); - if (hasBeenReordered(before, after, diffAdded, diffRemoved)) { + if (hasBeenReordered(before, after, added, removed)) { LoggedAction.build().source(this.sender).target(track) .description("webeditor", "reorder", after) .build().submit(this.plugin, this.sender); } - for (String n : diffAdded) { + for (String n : added) { LoggedAction.build().source(this.sender).target(track) .description("webeditor", "add", n) .build().submit(this.plugin, this.sender); } - for (String n : diffRemoved) { + for (String n : removed) { LoggedAction.build().source(this.sender).target(track) .description("webeditor", "remove", n) .build().submit(this.plugin, this.sender); } Message.APPLY_EDITS_SUCCESS.send(this.sender, "track", Component.text(track.getName())); - Message.APPLY_EDITS_SUCCESS_SUMMARY.send(this.sender, additions, deletions); + Message.APPLY_EDITS_SUCCESS_SUMMARY.send(this.sender, added.size(), removed.size()); Message.APPLY_EDITS_TRACK_BEFORE.send(this.sender, before); Message.APPLY_EDITS_TRACK_AFTER.send(this.sender, after); @@ -306,6 +368,10 @@ private boolean applyUserDelete(JsonElement changeInfo) { .description("webeditor", "delete") .build().submit(this.plugin, this.sender); + if (this.session != null) { + this.session.excludeDeletedUser(user); + } + return true; } @@ -341,6 +407,10 @@ private boolean applyGroupDelete(JsonElement changeInfo) { .description("webeditor", "delete") .build().submit(this.plugin, this.sender); + if (this.session != null) { + this.session.excludeDeletedGroup(group); + } + return true; } @@ -371,19 +441,11 @@ private boolean applyTrackDelete(JsonElement changeInfo) { .description("webeditor", "delete") .build().submit(this.plugin, this.sender); - return true; - } - - private static Set getAdded(Collection before, Collection after) { - Set added = new LinkedHashSet<>(after); - added.removeAll(before); - return added; - } + if (this.session != null) { + this.session.excludeDeletedTrack(track); + } - private static Set getRemoved(Collection before, Collection after) { - Set removed = new LinkedHashSet<>(before); - removed.removeAll(after); - return removed; + return true; } private static boolean hasBeenReordered(List before, List after, Collection diffAdded, Collection diffRemoved) { 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 new file mode 100644 index 000000000..67d5c8f39 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/WebEditorSession.java @@ -0,0 +1,220 @@ +/* + * 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.webeditor; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.http.AbstractHttpClient; +import me.lucko.luckperms.common.http.UnsuccessfulRequestException; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.PermissionHolder; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; +import me.lucko.luckperms.common.model.Track; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Encapsulates a session with the web editor. + * + *

    A session is tied to a specific user, and can comprise of multiple requests to and + * responses from the web editor.

    + */ +public class WebEditorSession { + + public static WebEditorSession create(List holders, List tracks, Sender sender, String cmdLabel, LuckPermsPlugin plugin) { + WebEditorRequest initialRequest = WebEditorRequest.generate(holders, tracks, sender, cmdLabel, plugin); + return new WebEditorSession(initialRequest, plugin, sender, cmdLabel); + } + + private WebEditorRequest initialRequest; + + private final LuckPermsPlugin plugin; + private final Sender sender; + private final String cmdLabel; + + private final Set holders; + private final Set tracks; + + private WebEditorSocket socket = null; + + public WebEditorSession(WebEditorRequest initialRequest, LuckPermsPlugin plugin, Sender sender, String cmdLabel) { + this.initialRequest = initialRequest; + this.plugin = plugin; + this.sender = sender; + this.cmdLabel = cmdLabel; + + this.holders = new LinkedHashSet<>(initialRequest.getHolders().keySet()); + this.tracks = new LinkedHashSet<>(initialRequest.getTracks().keySet()); + } + + public String open() { + createSocket(); + return createInitialSession(); + } + + private void createSocket() { + try { + // create and connect to a socket + WebEditorSocket socket = new WebEditorSocket(this.plugin, this.sender, this); + socket.initialize(this.plugin.getBytesocks()); + socket.waitForConnect(5, TimeUnit.SECONDS); + + this.socket = socket; + this.plugin.getWebEditorStore().sockets().putSocket(this.sender, this.socket); + } catch (Exception e) { + if (!ignoreSocketConnectError(e)) { + this.plugin.getLogger().warn("Unable to establish socket connection", e); + } + } + } + + private static boolean ignoreSocketConnectError(Exception e) { + if (e instanceof UnsuccessfulRequestException) { + UnsuccessfulRequestException req = (UnsuccessfulRequestException) e; + int code = req.getResponse().code(); + + // 502 - bad gateway / 503 - service unavailable + // probably means the socket service is offline, that's ok, no need to send a warning + return code == 502 || code == 503; + } + + return false; + } + + private String createInitialSession() { + Objects.requireNonNull(this.initialRequest); + + WebEditorRequest request = this.initialRequest; + this.initialRequest = null; + + if (this.socket != null) { + this.socket.appendDetailToRequest(request); + } + + String id = uploadRequestData(request); + if (id == null) { + return null; + } + + // form a url for the editor + String url = this.plugin.getConfiguration().get(ConfigKeys.WEB_EDITOR_URL_PATTERN) + id; + Message.EDITOR_URL.send(this.sender, url); + + // schedule socket close + if (this.socket != null) { + this.socket.scheduleCleanupIfUnused(); + } + + return id; + } + + public WebEditorSocket getSocket() { + return this.socket; + } + + public void includeCreatedGroup(Group group) { + this.holders.add(group.getIdentifier()); + } + + public void includeCreatedTrack(Track track) { + this.tracks.add(track.getName()); + } + + public void excludeDeletedUser(User user) { + this.holders.remove(user.getIdentifier()); + } + + public void excludeDeletedGroup(Group group) { + this.holders.remove(group.getIdentifier()); + } + + public void excludeDeletedTrack(Track track) { + this.tracks.remove(track.getName()); + } + + public String createFollowUpSession() { + List holders = this.holders.stream() + .map(id -> { + switch (id.getType()) { + case PermissionHolderIdentifier.USER_TYPE: + return this.plugin.getStorage().loadUser(UUID.fromString(id.getName()), null); + case PermissionHolderIdentifier.GROUP_TYPE: + return this.plugin.getStorage().loadGroup(id.getName()).thenApply(o -> o.orElse(null)); + default: + return null; + } + }) + .filter(Objects::nonNull) + .map(CompletableFuture::join) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + List tracks = this.tracks.stream() + .map(id -> this.plugin.getStorage().loadTrack(id).thenApply(o -> o.orElse(null))) + .map(CompletableFuture::join) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + return uploadRequestData(WebEditorRequest.generate(holders, tracks, this.sender, this.cmdLabel, this.plugin)); + } + + public String getCommandLabel() { + return this.cmdLabel; + } + + private String uploadRequestData(WebEditorRequest request) { + byte[] requestBuf = request.encode(); + + String pasteId; + try { + pasteId = this.plugin.getBytebin().postContent(requestBuf, AbstractHttpClient.JSON_TYPE, "editor").key(); + } catch (UnsuccessfulRequestException e) { + Message.EDITOR_HTTP_REQUEST_FAILURE.send(this.sender, e.getResponse().code(), e.getResponse().message()); + return null; + } catch (IOException e) { + new RuntimeException("Error uploading data to bytebin", e).printStackTrace(); + Message.EDITOR_HTTP_UNKNOWN_FAILURE.send(this.sender); + return null; + } + + this.plugin.getWebEditorStore().sessions().addNewSession(pasteId, request); + return pasteId; + } + + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithm.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithm.java new file mode 100644 index 000000000..6526061e7 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithm.java @@ -0,0 +1,171 @@ +/* + * 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.webeditor.socket; + +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * The signature algorithm & public/private key crypto logic used by the web editor socket connection. + */ +public enum SignatureAlgorithm { + V1_RSA(1, "RSA", "SHA256withRSA") { + @Override + public KeyPair generateKeyPair() { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(4096); + return generator.generateKeyPair(); + } catch (Exception e) { + throw new RuntimeException("Exception generating keypair", e); + } + } + }, + V2_ECDSA(2, "EC", "SHA256withECDSAinP1363Format") { + @Override + public KeyPair generateKeyPair() { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(new ECGenParameterSpec("secp256r1")); + return generator.generateKeyPair(); + } catch (Exception e) { + throw new RuntimeException("Exception generating keypair", e); + } + } + }; + + /** + * The selected {@link SignatureAlgorithm} for the current environment. + */ + public static final SignatureAlgorithm INSTANCE; + + static { + // select an instance to use based on the available algorithms + SignatureAlgorithm instance = V1_RSA; + try { + KeyPairGenerator.getInstance(V2_ECDSA.keyFactoryAlgorithm); + Signature.getInstance(V2_ECDSA.signatureAlgorithm); + instance = V2_ECDSA; + } catch (Exception e) { + // ignore + } + INSTANCE = instance; + } + + private final int protocolVersion; + private final String keyFactoryAlgorithm; + private final String signatureAlgorithm; + + SignatureAlgorithm(int protocolVersion, String keyFactoryAlgorithm, String signatureAlgorithm) { + this.protocolVersion = protocolVersion; + this.keyFactoryAlgorithm = keyFactoryAlgorithm; + this.signatureAlgorithm = signatureAlgorithm; + } + + /** + * Gets the corresponding protocol version + * + * @return the protocol version + */ + public int protocolVersion() { + return this.protocolVersion; + } + + /** + * Parse a public key from the given string. + * + * @param base64String a base64 string encoding the public key + * @return the parsed public key + * @throws IllegalArgumentException if the input was invalid + */ + public PublicKey parsePublicKey(String base64String) throws IllegalArgumentException { + try { + byte[] bytes = Base64.getDecoder().decode(base64String); + X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes); + KeyFactory rsa = KeyFactory.getInstance(this.keyFactoryAlgorithm); + return rsa.generatePublic(spec); + } catch (Exception e) { + throw new IllegalArgumentException("Exception parsing public key", e); + } + } + + /** + * Generate a public/private key pair. + * + * @return the generated key pair + */ + public abstract KeyPair generateKeyPair(); + + /** + * Signs {@code msg} using the given {@link PrivateKey}. + * + * @param privateKey the private key to sign with + * @param msg the message + * @return a base64 string containing the signature + */ + public String sign(PrivateKey privateKey, String msg) { + try { + Signature sign = Signature.getInstance(this.signatureAlgorithm); + sign.initSign(privateKey); + sign.update(msg.getBytes(StandardCharsets.UTF_8)); + + return Base64.getEncoder().encodeToString(sign.sign()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Verify that the given base64 encoded signature matches + * the given message and {@link PublicKey}. + * + * @param publicKey the public key that the message was supposedly signed with + * @param msg the message + * @param signatureBase64 the provided signature + * @return true if the signature is ok + */ + public boolean verify(PublicKey publicKey, String msg, String signatureBase64) { + try { + Signature sign = Signature.getInstance(this.signatureAlgorithm); + sign.initVerify(publicKey); + sign.update(msg.getBytes(StandardCharsets.UTF_8)); + + byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64); + return sign.verify(signatureBytes); + } catch (Exception e) { + return false; + } + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/SocketMessageType.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/SocketMessageType.java new file mode 100644 index 000000000..2b116ad09 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/SocketMessageType.java @@ -0,0 +1,78 @@ +/* + * 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.webeditor.socket; + +import me.lucko.luckperms.common.util.ImmutableCollectors; +import me.lucko.luckperms.common.util.gson.JObject; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; + +public enum SocketMessageType { + + /** Sent when the editor first says "hello" over the channel. (editor -> plugin) */ + HELLO("hello"), + + /** Sent when the plugin replies to the editors "hello" message. (plugin -> editor) */ + HELLO_REPLY("hello-reply"), + + /** Sent by the editor to confirm that a connection has been established. (editor -> plugin) */ + CONNECTED("connected"), + + /** Sent by the editor to request that the plugin applies a change. (editor -> plugin) */ + CHANGE_REQUEST("change-request"), + + /** Sent by the plugin to confirm that the changes sent by the editor have been accepted or applied. (plugin -> editor) */ + CHANGE_RESPONSE("change-response"), + + /** Ping message to keep the socket alive. (editor -> plugin) */ + PING("ping"), + + /** Ping response. (plugin -> editor) */ + PONG("pong"); + + public final String id; + + SocketMessageType(String id) { + this.id = id; + } + + public JObject builder() { + return new JObject().add("type", this.id); + } + + private static final Map LOOKUP = Arrays.stream(SocketMessageType.values()) + .collect(ImmutableCollectors.toMap(m -> m.id, Function.identity())); + + public static SocketMessageType getById(String id) { + SocketMessageType type = LOOKUP.get(id); + if (type == null) { + throw new IllegalArgumentException(id); + } + return type; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/WebEditorSocket.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/WebEditorSocket.java new file mode 100644 index 000000000..6173349d7 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/WebEditorSocket.java @@ -0,0 +1,256 @@ +/* + * 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.webeditor.socket; + +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.http.BytesocksClient; +import me.lucko.luckperms.common.http.UnsuccessfulRequestException; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.gson.GsonProvider; +import me.lucko.luckperms.common.util.gson.JObject; +import me.lucko.luckperms.common.webeditor.WebEditorRequest; +import me.lucko.luckperms.common.webeditor.WebEditorSession; +import me.lucko.luckperms.common.webeditor.socket.listener.WebEditorSocketListener; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Base64; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class WebEditorSocket { + + /** The plugin */ + private final LuckPermsPlugin plugin; + /** The sender who created the editor session */ + private final Sender sender; + /** The web editor session */ + private final WebEditorSession session; + /** The socket listener that handles incoming messages */ + private final WebEditorSocketListener listener; + /** The public and private keys used to sign messages sent by the plugin */ + private final KeyPair pluginKeyPair; + + /** The websocket backing the connection */ + private BytesocksClient.Socket socket; + /** A task to check if the socket is still active */ + private SchedulerTask keepaliveTask; + /** The public key used by the editor to sign messages */ + private PublicKey remotePublicKey; + /** If the connection is closed */ + private boolean closed = false; + + public WebEditorSocket(LuckPermsPlugin plugin, Sender sender, WebEditorSession session) { + this.plugin = plugin; + this.sender = sender; + this.session = session; + this.listener = new WebEditorSocketListener(this); + this.pluginKeyPair = plugin.getWebEditorStore().keyPair(); + } + + /** + * Initializes the socket connection. + * + * @param client the bytesocks client to connect to + * @throws UnsuccessfulRequestException if the request fails + * @throws IOException if an i/o error occurs + */ + public void initialize(BytesocksClient client) throws UnsuccessfulRequestException, IOException { + this.socket = client.createSocket(this.listener); + } + + /** + * Waits the specified amount of time for the socket to connect, + * before throwing an exception if a timeout occurs. + * + * @param timeout the timeout + * @param unit the timeout unit + */ + public void waitForConnect(long timeout, TimeUnit unit) { + try { + this.listener.connectFuture().get(timeout, unit); + } catch (ExecutionException | TimeoutException | InterruptedException e) { + throw new RuntimeException("Timed out waiting to socket to connect", e); + } + } + + /** + * Adds detail about the socket channel and the plugin public key to + * the editor request payload that gets sent via bytebin to the viewer. + * + * @param request the request + */ + public void appendDetailToRequest(WebEditorRequest request) { + String channelId = this.socket.channelId(); + String publicKey = Base64.getEncoder().encodeToString(this.pluginKeyPair.getPublic().getEncoded()); + + JsonObject socket = new JsonObject(); + socket.addProperty("protocolVersion", SignatureAlgorithm.INSTANCE.protocolVersion()); + socket.addProperty("channelId", channelId); + socket.addProperty("publicKey", publicKey); + + JsonObject payload = request.getPayload(); + payload.add("socket", socket); + } + + /** + * Send a message to the socket. + * + *

    The message will be encoded as JSON and + * signed using the public public key.

    + * + * @param msg the message + */ + public void send(JsonObject msg) { + String encoded = GsonProvider.normal().toJson(msg); + String signature = SignatureAlgorithm.INSTANCE.sign(this.pluginKeyPair.getPrivate(), encoded); + + JsonObject frame = new JObject() + .add("msg", encoded) + .add("signature", signature) + .toJson(); + + this.socket.socket().send(GsonProvider.normal().toJson(frame)); + } + + public boolean trustConnection(String nonce) { + if (this.listener.shouldIgnoreMessages()) { + return false; + } + + if (this.remotePublicKey != null) { + return false; + } + + PublicKey publicKey = this.listener.helloHandler().getAttemptedConnection(nonce); + if (publicKey == null) { + return false; + } + + this.remotePublicKey = publicKey; + + // save the key in the keystore + this.plugin.getWebEditorStore().keystore().trust(this.sender, this.remotePublicKey.getEncoded()); + + // send a reply back to the editor to say that it is now trusted + send(SocketMessageType.HELLO_REPLY.builder() + .add("nonce", nonce) + .add("state", "trusted") + .toJson() + ); + return true; + } + + public void scheduleCleanupIfUnused() { + this.plugin.getBootstrap().getScheduler().asyncLater(this::afterOpenFor1Minute, 1, TimeUnit.MINUTES); + } + + private void afterOpenFor1Minute() { + if (this.closed) { + return; + } + + if (this.remotePublicKey == null && !this.listener.helloHandler().hasAttemptedConnection()) { + // If the editor hasn't made an initial connection after 1 minute, + // then close + stop listening to the socket. + closeSocket(); + } else { + // Otherwise, setup a keepalive monitoring task + this.keepaliveTask = this.plugin.getBootstrap().getScheduler().asyncRepeating(this::keepalive, 10, TimeUnit.SECONDS); + } + } + + /** + * The keepalive tasks checks to see when the last ping from the editor was. If the editor + * hasn't sent anything for 1 minute, then close the connection + */ + private void keepalive() { + if (System.currentTimeMillis() - this.listener.pingHandler().getLastPing() > TimeUnit.MINUTES.toMillis(1)) { + cancelKeepalive(); + closeSocket(); + } + } + + public void close() { + try { + send(SocketMessageType.PONG.builder() + .add("ok", false) + .toJson() + ); + } catch (Exception e) { + // ignore + } + + cancelKeepalive(); + closeSocket(); + } + + private void closeSocket() { + this.socket.socket().close(1000, "Normal"); + this.plugin.getWebEditorStore().sockets().removeSocket(this); + this.closed = true; + } + + private void cancelKeepalive() { + if (this.keepaliveTask != null) { + this.keepaliveTask.cancel(); + this.keepaliveTask = null; + } + } + + public LuckPermsPlugin getPlugin() { + return this.plugin; + } + + public Sender getSender() { + return this.sender; + } + + public WebEditorSession getSession() { + return this.session; + } + + public BytesocksClient.Socket getSocket() { + return this.socket; + } + + public PublicKey getRemotePublicKey() { + return this.remotePublicKey; + } + + public void setRemotePublicKey(PublicKey remotePublicKey) { + this.remotePublicKey = remotePublicKey; + } + + public boolean isClosed() { + return this.closed; + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/Handler.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/Handler.java new file mode 100644 index 000000000..e1170f214 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/Handler.java @@ -0,0 +1,37 @@ +/* + * 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.webeditor.socket.listener; + +import com.google.gson.JsonObject; + +/** + * A handler for a given type of message. + */ +public interface Handler { + + void handle(JsonObject msg); + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerChangeRequest.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerChangeRequest.java new file mode 100644 index 000000000..fe8bc20a2 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerChangeRequest.java @@ -0,0 +1,104 @@ +/* + * 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.webeditor.socket.listener; + +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.command.access.CommandPermission; +import me.lucko.luckperms.common.http.UnsuccessfulRequestException; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.webeditor.WebEditorResponse; +import me.lucko.luckperms.common.webeditor.socket.SocketMessageType; +import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; + +import java.io.IOException; +import java.util.Objects; + +/** + * Handler for {@link SocketMessageType#CHANGE_REQUEST} + */ +public class HandlerChangeRequest implements Handler { + + /** The change has been accepted, and the plugin will now apply it. */ + private static final String STATE_ACCEPTED = "accepted"; + /** The change has been applied. */ + private static final String STATE_APPLIED = "applied"; + + /** The socket */ + private final WebEditorSocket socket; + + public HandlerChangeRequest(WebEditorSocket socket) { + this.socket = socket; + } + + @Override + public void handle(JsonObject msg) { + if (!this.socket.getSender().hasPermission(CommandPermission.APPLY_EDITS)) { + throw new IllegalStateException("Sender does not have applyedits permission"); + } + + // get the bytebin code containing the editor data + String code = msg.get("code").getAsString(); + if (code == null || code.isEmpty()) { + throw new IllegalArgumentException("Invalid code"); + } + + // send "change-accepted" response + this.socket.getPlugin().getBootstrap().getScheduler().executeAsync(() -> + this.socket.send(SocketMessageType.CHANGE_RESPONSE.builder() + .add("state", STATE_ACCEPTED) + .toJson() + ) + ); + + // download data from bytebin + JsonObject data; + try { + data = this.socket.getPlugin().getBytebin().getJsonContent(code).getAsJsonObject(); + Objects.requireNonNull(data); + } catch (UnsuccessfulRequestException | IOException e) { + throw new RuntimeException("Error reading data", e); + } + + // apply changes + Message.EDITOR_SOCKET_CHANGES_RECEIVED.send(this.socket.getSender()); + new WebEditorResponse(code, data).apply( + this.socket.getPlugin(), + this.socket.getSender(), + this.socket.getSession(), + "lp", + false + ); + + // create a new session + String newSessionCode = this.socket.getSession().createFollowUpSession(); + this.socket.send(SocketMessageType.CHANGE_RESPONSE.builder() + .add("state", STATE_APPLIED) + .add("newSessionCode", newSessionCode) + .toJson() + ); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerConnected.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerConnected.java new file mode 100644 index 000000000..204eae250 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerConnected.java @@ -0,0 +1,49 @@ +/* + * 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.webeditor.socket.listener; + +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.webeditor.socket.SocketMessageType; +import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; + +/** + * Handler for {@link SocketMessageType#CONNECTED} + */ +public class HandlerConnected implements Handler { + + /** The socket */ + private final WebEditorSocket socket; + + public HandlerConnected(WebEditorSocket socket) { + this.socket = socket; + } + + @Override + public void handle(JsonObject msg) { + Message.EDITOR_SOCKET_CONNECTED.send(this.socket.getSender()); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerHello.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerHello.java new file mode 100644 index 000000000..bb4cd7851 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerHello.java @@ -0,0 +1,129 @@ +/* + * 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.webeditor.socket.listener; + +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.webeditor.socket.SignatureAlgorithm; +import me.lucko.luckperms.common.webeditor.socket.SocketMessageType; +import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; +import me.lucko.luckperms.common.webeditor.store.RemoteSession; + +import java.security.PublicKey; +import java.util.HashMap; +import java.util.Map; + +/** + * Handler for {@link SocketMessageType#HELLO} + */ +public class HandlerHello implements Handler { + + /** The session is accepted, the editor public key is already known so no further action is needed */ + private static final String STATE_ACCEPTED = "accepted"; + /** The session is accepted, but the user needs to confirm in-game before any changes will be accepted. */ + private static final String STATE_UNTRUSTED = "untrusted"; + /** A session has already been established with a different identity */ + private static final String STATE_REJECTED = "rejected"; + /** The remote editor session is based off session data which has already been applied */ + private static final String STATE_INVALID = "invalid"; + + /** The socket */ + private final WebEditorSocket socket; + + /** A list of attempted connections (connections that have been attempted with an untrusted public key) */ + private final Map attemptedConnections = new HashMap<>(); + + public HandlerHello(WebEditorSocket socket) { + this.socket = socket; + } + + public PublicKey getAttemptedConnection(String nonce) { + return this.attemptedConnections.get(nonce); + } + + public boolean hasAttemptedConnection() { + return !this.attemptedConnections.isEmpty(); + } + + @Override + public void handle(JsonObject msg) { + String nonce = getStringOrThrow(msg, "nonce"); + String sessionId = getStringOrThrow(msg, "sessionId"); + String browser = msg.get("browser").getAsString(); + PublicKey remotePublicKey = SignatureAlgorithm.INSTANCE.parsePublicKey(msg.get("publicKey").getAsString()); + + // check if the public keys are the same + // (this allows the same editor to re-connect, but prevents new connections) + if (this.socket.getRemotePublicKey() != null && !this.socket.getRemotePublicKey().equals(remotePublicKey)) { + sendReply(nonce, STATE_REJECTED); + return; + } + + // check if session is valid + RemoteSession session = this.socket.getPlugin().getWebEditorStore().sessions().getSession(sessionId); + if (session == null || session.isCompleted()) { + sendReply(nonce, STATE_INVALID); + return; + } + + // check if the public key is trusted + if (!this.socket.getPlugin().getWebEditorStore().keystore().isTrusted(this.socket.getSender(), remotePublicKey.getEncoded())) { + sendReply(nonce, STATE_UNTRUSTED); + + // ask the user if they want to trust the connection + Message.EDITOR_SOCKET_UNTRUSTED.send(this.socket.getSender(), nonce, browser, this.socket.getSession().getCommandLabel(), this.socket.getSender().isConsole()); + this.attemptedConnections.put(nonce, remotePublicKey); + return; + } + + boolean reconnected = this.socket.getRemotePublicKey() != null; + this.socket.setRemotePublicKey(remotePublicKey); + + sendReply(nonce, STATE_ACCEPTED); + + if (reconnected) { + Message.EDITOR_SOCKET_RECONNECTED.send(this.socket.getSender()); + } else { + Message.EDITOR_SOCKET_CONNECTED.send(this.socket.getSender()); + } + } + + private void sendReply(String nonce, String state) { + this.socket.send(SocketMessageType.HELLO_REPLY.builder() + .add("nonce", nonce) + .add("state", state) + .toJson() + ); + } + + private static String getStringOrThrow(JsonObject msg, String key) { + String val = msg.get(key).getAsString(); + if (val == null || val.isEmpty()) { + throw new IllegalStateException("missing " + key); + } + return val; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerPing.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerPing.java new file mode 100644 index 000000000..e17f33f42 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/HandlerPing.java @@ -0,0 +1,59 @@ +/* + * 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.webeditor.socket.listener; + +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.webeditor.socket.SocketMessageType; +import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; + +/** + * Handler for {@link SocketMessageType#PING} + */ +public class HandlerPing implements Handler { + + /** The socket */ + private final WebEditorSocket socket; + + /** The time a ping was last received */ + private long lastPing = 0; + + public HandlerPing(WebEditorSocket socket) { + this.socket = socket; + } + + public long getLastPing() { + return this.lastPing; + } + + @Override + public void handle(JsonObject msg) { + this.lastPing = System.currentTimeMillis(); + this.socket.send(SocketMessageType.PONG.builder() + .add("ok", true) + .toJson() + ); + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/WebEditorSocketListener.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/WebEditorSocketListener.java new file mode 100644 index 000000000..0e7bb1010 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/socket/listener/WebEditorSocketListener.java @@ -0,0 +1,178 @@ +/* + * 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.webeditor.socket.listener; + +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.util.gson.GsonProvider; +import me.lucko.luckperms.common.webeditor.socket.SignatureAlgorithm; +import me.lucko.luckperms.common.webeditor.socket.SocketMessageType; +import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.io.EOFException; +import java.security.PublicKey; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReentrantLock; + +public class WebEditorSocketListener extends WebSocketListener { + + /** The socket */ + private final WebEditorSocket socket; + + // Individual handlers for each message type + private final HandlerHello helloHandler; + private final HandlerConnected connectedHandler; + private final HandlerPing pingHandler; + private final HandlerChangeRequest changeRequestHandler; + + /** A future that will complete when the connection is established successfully */ + private final CompletableFuture connectFuture = new CompletableFuture<>(); + + /** Message receive lock */ + private final ReentrantLock lock = new ReentrantLock(); + + public WebEditorSocketListener(WebEditorSocket socket) { + this.socket = socket; + this.helloHandler = new HandlerHello(socket); + this.connectedHandler = new HandlerConnected(socket); + this.pingHandler = new HandlerPing(socket); + this.changeRequestHandler = new HandlerChangeRequest(socket); + } + + @Override + public void onOpen(@NonNull WebSocket webSocket, @NonNull Response response) { + this.connectFuture.complete(null); + } + + @Override + public void onFailure(@NonNull WebSocket webSocket, @NonNull Throwable e, Response response) { + if (e instanceof EOFException) { + return; // ignore + } + this.socket.getPlugin().getLogger().warn("Exception occurred in web socket", e); + } + + @Override + public void onMessage(@NonNull WebSocket webSocket, @NonNull String msg) { + this.socket.getPlugin().getBootstrap().getScheduler().executeAsync(() -> { + this.lock.lock(); + try { + if (shouldIgnoreMessages()) { + return; + } + + handleMessageFrame(msg); + } catch (Exception e) { + this.socket.getPlugin().getLogger().warn("Exception occurred handling message from socket", e); + } finally { + this.lock.unlock(); + } + }); + } + + /** + * Checks if incoming messages should be ignored. + * + * @return true if messages should be ignored + */ + public boolean shouldIgnoreMessages() { + if (this.socket.isClosed()) { + return true; + } + + if (!this.socket.getSender().isValid()) { + this.socket.close(); + return true; + } + + return false; + } + + private void handleMessageFrame(String stringMsg) { + JsonObject frame = GsonProvider.parser().parse(stringMsg).getAsJsonObject(); + + String innerMsg = frame.get("msg").getAsString(); + String signature = frame.get("signature").getAsString(); + + if (innerMsg == null || innerMsg.isEmpty() || signature == null || signature.isEmpty()) { + throw new IllegalArgumentException("Incomplete message"); + } + + // check signature to ensure the message is from the connected editor + PublicKey remotePublicKey = this.socket.getRemotePublicKey(); + boolean verified = remotePublicKey != null && SignatureAlgorithm.INSTANCE.verify(remotePublicKey, innerMsg, signature); + + // parse the inner message + JsonObject msg = GsonProvider.parser().parse(innerMsg).getAsJsonObject(); + SocketMessageType type = SocketMessageType.getById(msg.get("type").getAsString()); + + if (type == SocketMessageType.HELLO) { + this.helloHandler.handle(msg); + return; + } + + if (!verified) { + throw new IllegalStateException("Signature not accepted"); + } + + switch (type) { + case CHANGE_REQUEST: + this.changeRequestHandler.handle(msg); + break; + case CONNECTED: + this.connectedHandler.handle(msg); + break; + case PING: + this.pingHandler.handle(msg); + break; + default: + throw new IllegalStateException("Invalid message type: " + type); + } + } + + public CompletableFuture connectFuture() { + return this.connectFuture; + } + + public HandlerHello helloHandler() { + return this.helloHandler; + } + + public HandlerConnected connectedHandler() { + return this.connectedHandler; + } + + public HandlerPing pingHandler() { + return this.pingHandler; + } + + public HandlerChangeRequest changeRequestHandler() { + return this.changeRequestHandler; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/QueryField.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/RemoteSession.java similarity index 69% rename from common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/QueryField.java rename to common/src/main/java/me/lucko/luckperms/common/webeditor/store/RemoteSession.java index cb5af5d31..1759df8cc 100644 --- a/common/src/main/java/me/lucko/luckperms/common/bulkupdate/query/QueryField.java +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/RemoteSession.java @@ -23,32 +23,29 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.bulkupdate.query; +package me.lucko.luckperms.common.webeditor.store; -/** - * Represents a field being used in an update - */ -public enum QueryField { +import me.lucko.luckperms.common.webeditor.WebEditorRequest; - PERMISSION("permission"), - SERVER("server"), - WORLD("world"); +public final class RemoteSession { + private WebEditorRequest request; + private boolean completed; - private final String sqlName; + public RemoteSession(WebEditorRequest request) { + this.request = request; + this.completed = false; + } - public static QueryField of(String s) { - try { - return valueOf(s.toUpperCase()); - } catch (IllegalArgumentException e) { - return null; - } + public WebEditorRequest request() { + return this.request; } - QueryField(String sqlName) { - this.sqlName = sqlName; + public boolean isCompleted() { + return this.completed; } - public String getSqlName() { - return this.sqlName; + public void complete() { + this.completed = true; + this.request = null; } } diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorKeystore.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorKeystore.java new file mode 100644 index 000000000..44c6de0ad --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorKeystore.java @@ -0,0 +1,195 @@ +/* + * 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.webeditor.store; + +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.node.types.Meta; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.gson.GsonProvider; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.NodeType; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +public final class WebEditorKeystore { + private static final String META_KEY = "lp-editor-key"; + + private final Path consoleKeysPath; + private final Set trustedConsoleKeys; + + public WebEditorKeystore(Path consoleKeysPath) { + this.consoleKeysPath = consoleKeysPath; + this.trustedConsoleKeys = new CopyOnWriteArraySet<>(); + + try { + load(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Checks if the given public key has been trusted by the sender. + * + * @param sender the sender + * @param publicKey the public key + * @return true if trusted + */ + public boolean isTrusted(Sender sender, byte[] publicKey) { + return isTrusted(sender, hash(publicKey)); + } + + /** + * Checks if the given public key hash has been trusted by the sender. + * + * @param sender the sender + * @param hash the public key hash + * @return true if trusted + */ + public boolean isTrusted(Sender sender, String hash) { + if (sender.isConsole()) { + return isTrustedConsole(hash); + } else { + User user = sender.getPlugin().getUserManager().getIfLoaded(sender.getUniqueId()); + return user != null && isTrusted(user, hash); + } + } + + /** + * Trusts the given public key for the sender. + * + * @param sender the sender + * @param publicKey the public key + */ + public void trust(Sender sender, byte[] publicKey) { + trust(sender, hash(publicKey)); + } + + /** + * Trusts the given public key hash for the sender. + * + * @param sender the sender + * @param hash the public key hash + */ + public void trust(Sender sender, String hash) { + if (sender.isConsole()) { + trustConsole(hash); + } else { + User user = sender.getPlugin().getUserManager().getIfLoaded(sender.getUniqueId()); + if (user != null) { + trust(user, hash); + } + } + } + + // console + + private boolean isTrustedConsole(String hash) { + return this.trustedConsoleKeys.contains(hash); + } + + private void trustConsole(String hash) { + this.trustedConsoleKeys.add(hash); + + try { + save(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void load() throws Exception { + if (Files.exists(this.consoleKeysPath)) { + try (BufferedReader reader = Files.newBufferedReader(this.consoleKeysPath, StandardCharsets.UTF_8)) { + KeystoreFile file = GsonProvider.normal().fromJson(reader, KeystoreFile.class); + if (file != null && file.consoleKeys != null) { + this.trustedConsoleKeys.addAll(file.consoleKeys); + } + } + } + } + + private void save() throws Exception { + try (BufferedWriter writer = Files.newBufferedWriter(this.consoleKeysPath, StandardCharsets.UTF_8)) { + KeystoreFile file = new KeystoreFile(); + file.consoleKeys = new ArrayList<>(this.trustedConsoleKeys); + GsonProvider.prettyPrinting().toJson(file, writer); + } + } + + // users + + private boolean isTrusted(User user, String hash) { + String key = user.getCachedData().getMetaData(QueryOptionsImpl.DEFAULT_CONTEXTUAL) + .getMetaValue(META_KEY, CheckOrigin.INTERNAL).result(); + + if (key == null || key.isEmpty()) { + return false; + } + + return hash.equals(key); + } + + private void trust(User user, String hash) { + user.removeIf(DataType.NORMAL, ImmutableContextSetImpl.EMPTY, NodeType.META.predicate(mn -> mn.getMetaKey().equals(META_KEY)), false); + user.setNode(DataType.NORMAL, Meta.builder(META_KEY, hash).build(), false); + + user.getPlugin().getStorage().saveUser(user).join(); + } + + private static String hash(byte[] buf) { + byte[] digest = createDigest().digest(buf); + return Base64.getEncoder().encodeToString(digest); + } + + private static MessageDigest createDigest() { + try { + return MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings({"FieldMayBeFinal", "unused"}) + private static class KeystoreFile { + private String _comment = "This file stores a list of trusted editor public keys"; + private List consoleKeys = null; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorSessionMap.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorSessionMap.java new file mode 100644 index 000000000..7033a6b07 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorSessionMap.java @@ -0,0 +1,56 @@ +/* + * 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.webeditor.store; + +import me.lucko.luckperms.common.webeditor.WebEditorRequest; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class WebEditorSessionMap { + private final Map sessions = new ConcurrentHashMap<>(); + + /** + * Adds a newly created session to the store. + * + * @param id the id of the session + */ + public void addNewSession(String id, WebEditorRequest request) { + this.sessions.put(id, new RemoteSession(request)); + } + + /** + * Gets the session for the given session id. + * + * @param id the id of the session + * @return the session + */ + public @Nullable RemoteSession getSession(String id) { + return this.sessions.get(id); + } + +} diff --git a/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorSocketMap.java b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorSocketMap.java new file mode 100644 index 000000000..99e807010 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorSocketMap.java @@ -0,0 +1,56 @@ +/* + * 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.webeditor.store; + +import com.github.benmanes.caffeine.cache.Cache; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.CaffeineFactory; +import me.lucko.luckperms.common.webeditor.socket.WebEditorSocket; + +import java.util.Collection; +import java.util.UUID; + +public final class WebEditorSocketMap { + private final Cache sockets = CaffeineFactory.newBuilder() + .weakValues() + .build(); + + public WebEditorSocket getSocket(Sender sender) { + return this.sockets.getIfPresent(sender.getUniqueId()); + } + + public void putSocket(Sender sender, WebEditorSocket socket) { + this.sockets.put(sender.getUniqueId(), socket); + } + + public void removeSocket(WebEditorSocket socket) { + this.sockets.asMap().values().remove(socket); + } + + public Collection getSockets() { + return this.sockets.asMap().values(); + } +} 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 new file mode 100644 index 000000000..3e45f6d6f --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/webeditor/store/WebEditorStore.java @@ -0,0 +1,82 @@ +/* + * 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.webeditor.store; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.webeditor.socket.SignatureAlgorithm; + +import java.security.KeyPair; +import java.util.concurrent.CompletableFuture; + +/** + * Contains a store of known web editor sessions and provides a lookup function for + * trusted editor public keys. + */ +@SuppressWarnings("Guava") +public class WebEditorStore { + private final WebEditorSessionMap sessions; + private final WebEditorSocketMap sockets; + private final WebEditorKeystore keystore; + private final Supplier> keyPair; + + public WebEditorStore(LuckPermsPlugin plugin) { + this.sessions = new WebEditorSessionMap(); + this.sockets = new WebEditorSocketMap(); + this.keystore = new WebEditorKeystore(plugin.getBootstrap().getConfigDirectory().resolve("editor-keystore.json")); + + Supplier> keyPair = () -> CompletableFuture.supplyAsync( + SignatureAlgorithm.INSTANCE::generateKeyPair, + plugin.getBootstrap().getScheduler().async() + ); + + if (plugin.getConfiguration().get(ConfigKeys.EDITOR_LAZILY_GENERATE_KEY)) { + this.keyPair = Suppliers.memoize(keyPair); + } else { + CompletableFuture future = keyPair.get(); + this.keyPair = () -> future; + } + } + + public WebEditorSessionMap sessions() { + return this.sessions; + } + + public WebEditorSocketMap sockets() { + return this.sockets; + } + + public WebEditorKeystore keystore() { + return this.keystore; + } + + public KeyPair keyPair() { + return this.keyPair.get().join(); + } + +} diff --git a/common/src/main/resources/luckperms_en.properties b/common/src/main/resources/luckperms_en.properties index 0d5202ad5..193e9bc0d 100644 --- a/common/src/main/resources/luckperms_en.properties +++ b/common/src/main/resources/luckperms_en.properties @@ -1,11 +1,21 @@ luckperms.logs.actionlog-prefix=LOG luckperms.logs.verbose-prefix=VB +luckperms.logs.verbose.hover.type-key=Type +luckperms.logs.verbose.hover.origin-key=Origin +luckperms.logs.verbose.hover.cause-key=Cause +luckperms.logs.verbose.hover.context-key=Context +luckperms.logs.verbose.hover.thread-key=Thread +luckperms.logs.verbose.hover.trace-key=Trace +luckperms.logs.verbose.hover.processor-key=Processor +luckperms.logs.verbose.hover.overflow=and {0} more +luckperms.logs.verbose.rate-limit-exceeded=Notification rate limit exceeded. Some events are not being shown. Use {0} to see the full output luckperms.logs.export-prefix=EXPORT luckperms.commandsystem.available-commands=Use {0} to view available commands luckperms.commandsystem.command-not-recognised=Command not recognised luckperms.commandsystem.no-permission=You do not have permission to use this command! luckperms.commandsystem.no-permission-subcommands=You do not have permission to use any sub commands luckperms.commandsystem.already-executing-command=Another command is being executed, waiting for it to finish... +luckperms.commandsystem.commands-disabled=LuckPerms commands are disabled luckperms.commandsystem.usage.sub-commands-header=Sub Commands luckperms.commandsystem.usage.usage-header=Command Usage luckperms.commandsystem.usage.arguments-header=Arguments @@ -46,6 +56,7 @@ luckperms.duration.unit.seconds.plural={0} seconds luckperms.duration.unit.seconds.singular={0} second luckperms.duration.unit.seconds.short={0}s luckperms.duration.since={0} ago +luckperms.duration.date=Date luckperms.command.misc.invalid-code=Invalid code luckperms.command.misc.response-code-key=response code luckperms.command.misc.error-message-key=message @@ -54,7 +65,10 @@ luckperms.command.misc.webapp-unable-to-communicate=Unable to communicate with t luckperms.command.misc.check-console-for-errors=Check the console for errors luckperms.command.misc.file-must-be-in-data=File {0} must be a direct child of the data directory luckperms.command.misc.wait-to-finish=Please wait for it to finish and try again -luckperms.command.misc.permission-invalid-empty=The empty string is not a valid permission +luckperms.command.misc.invalid-input-empty-permission=The empty string is not a valid permission +luckperms.command.misc.invalid-input-empty-meta-key=The empty string is not a valid meta key +luckperms.command.misc.invalid-input-empty-display-name=The empty string is not a valid display name +luckperms.command.misc.shorthand-parse-error=Warning: Permission {0} could not be parsed as shorthand: {1} luckperms.command.misc.invalid-priority=Invalid priority {0} luckperms.command.misc.expected-number=Expected a number luckperms.command.misc.date-parse-error=Could not parse date {0} @@ -80,9 +94,29 @@ luckperms.command.misc.loading.error.all-tracks=Unable to load all tracks luckperms.command.misc.loading.error.track-not-found=A track named {0} could not be found luckperms.command.misc.loading.error.track-save-error=There was an error whilst saving track data for {0} luckperms.command.misc.loading.error.track-invalid={0} is not a valid track name +luckperms.command.misc.node.permission=Permission +luckperms.command.misc.node.inheritance=Inheritance +luckperms.command.misc.node.prefix=Prefix +luckperms.command.misc.node.suffix=Suffix +luckperms.command.misc.node.meta=Meta +luckperms.command.misc.node.weight=Weight +luckperms.command.misc.node.displayname=Display Name +luckperms.command.misc.priority-label=priority: {0} luckperms.command.editor.no-match=Unable to open editor, no objects matched the desired type luckperms.command.editor.start=Preparing a new editor session, please wait... luckperms.command.editor.url=Click the link below to open the editor +luckperms.command.editor.socket.connected=Editor window connected successfully +luckperms.command.editor.socket.reconnected=Editor window reconnected successfully +luckperms.command.editor.socket.changes-received=Changes have been received from the connected web editor session +luckperms.command.editor.socket.untrusted=An editor window has connected, but it is not yet trusted +luckperms.command.editor.socket.untrusted.prompt.click=If it was you, {0} to trust the session! +luckperms.command.editor.socket.untrusted.prompt.click.action=click here +luckperms.command.editor.socket.untrusted.prompt.runcommand=If it was you, run {0} to trust the session! +luckperms.command.editor.socket.untrusted.sessioninfo=session id = {0}, browser = {1} +luckperms.command.editor.socket.trust.success=The editor session has been marked as trusted +luckperms.command.editor.socket.trust.futureinfo=In the future, connections from the same browser will be trusted automatically +luckperms.command.editor.socket.trust.connecting=The plugin will now attempt to establish a connection with the editor... +luckperms.command.editor.socket.trust.failure=Unable to trust the given session because the socket is closed, or because a different connection was established instead luckperms.command.editor.unable-to-communicate=Unable to communicate with the editor luckperms.command.editor.apply-edits.success=Web editor data was applied to {0} {1} successfully luckperms.command.editor.apply-edits.success-summary={0} {1} and {2} {3} @@ -93,6 +127,11 @@ luckperms.command.editor.apply-edits.success.deletions-singular=deletion luckperms.command.editor.apply-edits.no-changes=No changes were applied from the web editor, the returned data didn''t contain any edits luckperms.command.editor.apply-edits.unknown-type=Unable to apply edit to the specified object type luckperms.command.editor.apply-edits.unable-to-read=Unable to read data using the given code +luckperms.command.editor.apply-edits.unknown-session=The changes received from the web editor were not made in a session started on this server! +luckperms.command.editor.apply-edits.right-server-question=Are you sure you''re running the {0} command in the right place? +luckperms.command.editor.apply-edits.already-applied=The changes received from the web editor are based on an initial session which has already been applied! +luckperms.command.editor.apply-edits.how-to-avoid-conflicts=To avoid conflicts, you should never re-use the same editor session after the changes from it have been applied once already +luckperms.command.editor.apply-edits.bypass-warning=To ignore this warning and apply the changes anyway, run luckperms.command.search.searching.permission=Searching for users and groups with {0} luckperms.command.search.searching.inherit=Searching for users and groups who inherit from {0} luckperms.command.search.result=Found {0} entries from {1} users and {2} groups @@ -122,7 +161,6 @@ luckperms.command.info.server-brand-key=Server Brand luckperms.command.info.server-version-key=Server Version luckperms.command.info.storage-key=Storage luckperms.command.info.storage-type-key=Type -luckperms.command.info.storage.meta.split-types-key=Types luckperms.command.info.storage.meta.ping-key=Ping luckperms.command.info.storage.meta.connected-key=Connected luckperms.command.info.storage.meta.file-size-key=File Size @@ -232,8 +270,15 @@ luckperms.command.generic.contextual-data.null-result=None luckperms.command.user.info.title=User Info luckperms.command.user.info.uuid-key=UUID luckperms.command.user.info.uuid-type-key=type -luckperms.command.user.info.uuid-type.mojang=mojang +luckperms.command.user.info.uuid-type.mojang=official luckperms.command.user.info.uuid-type.not-mojang=offline +luckperms.command.user.info.uuid-type.npc=npc +luckperms.command.user.info.uuid-type.unknown=unknown +luckperms.command.user.info.uuid-type.desc.mojang=This is a unique id for an official Minecraft: Java Edition account (online mode) +luckperms.command.user.info.uuid-type.desc.not-mojang=This is a pseudo unique id generated from the username (offline mode) +luckperms.command.user.info.uuid-type.desc.npc=This is a unique id for a NPC (non-player character) +luckperms.command.user.info.uuid-type.desc.unknown=This is an unknown unique id type +luckperms.command.user.info.uuid-type.desc.api=This is a unique id type specified via the LuckPerms API luckperms.command.user.info.status-key=Status luckperms.command.user.info.status.online=Online luckperms.command.user.info.status.offline=Offline @@ -319,6 +364,7 @@ luckperms.command.import.progress.operations={0}/{1} operations complete luckperms.command.import.starting=Starting import process luckperms.command.import.completed=COMPLETED luckperms.command.import.duration=took {0} seconds +luckperms.command.bulkupdate.disabled=Bulk update functionality is disabled in the configuration file luckperms.command.bulkupdate.must-use-console=The bulk update command can only be used from the console luckperms.command.bulkupdate.invalid-data-type=Invalid type, was expecting {0} luckperms.command.bulkupdate.invalid-constraint=Invalid constraint {0} @@ -395,6 +441,8 @@ luckperms.usage.translations.argument.install=subcommand to install translations luckperms.usage.apply-edits.description=Applies permission changes made from the web editor luckperms.usage.apply-edits.argument.code=the unique code for the data luckperms.usage.apply-edits.argument.target=who to apply the data to +luckperms.usage.trust-editor.description=Trusts an editor session to apply changes without a confirmation +luckperms.usage.trust-editor.argument.id=the id of the session to trust luckperms.usage.create-group.description=Create a new group luckperms.usage.create-group.argument.name=the name of the group luckperms.usage.create-group.argument.weight=the weight of the group @@ -402,6 +450,7 @@ luckperms.usage.create-group.argument.display-name=the display name of the group luckperms.usage.delete-group.description=Delete a group luckperms.usage.delete-group.argument.name=the name of the group luckperms.usage.list-groups.description=List all groups on the platform +luckperms.usage.list-groups.argument.page=the page to view luckperms.usage.create-track.description=Create a new track luckperms.usage.create-track.argument.name=the name of the track luckperms.usage.delete-track.description=Delete a track @@ -423,6 +472,7 @@ luckperms.usage.user-clone.argument.user=the name/uuid of the user to clone onto luckperms.usage.group-info.description=Gives info about the group luckperms.usage.group-listmembers.description=Show the users/groups who inherit from this group luckperms.usage.group-listmembers.argument.page=the page to view +luckperms.usage.group-listmembers.argument.context=the context to filter members by luckperms.usage.group-setweight.description=Set the groups weight luckperms.usage.group-setweight.argument.weight=the weight to set luckperms.usage.group-set-display-name.description=Set the groups display name diff --git a/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterMongoTest.java b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterMongoTest.java new file mode 100644 index 000000000..f3920b67d --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterMongoTest.java @@ -0,0 +1,89 @@ +/* + * 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.actionlog; + +import me.lucko.luckperms.common.actionlog.filter.ActionFilterMongoBuilder; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import org.bson.BsonDocument; +import org.bson.UuidRepresentation; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.conversions.Bson; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ActionFilterMongoTest { + + private static Stream testQueries() { + return Stream.of( + Arguments.of( + ActionFilters.source(UUID.fromString("725d585e-4ff1-4f18-acca-6ac538364080")), + // {"$and": [{"source.uniqueId": {"$binary": {"base64": "cl1YXk/xTxisymrFODZAgA==", "subType": "04"}}}]} + "{\"$and\": [{\"source.uniqueId\": {\"$binary\": {\"base64\": \"cl1YXk/xTxisymrFODZAgA==\", \"subType\": \"04\"}}}]}" + ), + Arguments.of( + ActionFilters.user(UUID.fromString("725d585e-4ff1-4f18-acca-6ac538364080")), + // {"$and": [{"target.type": "USER"}, {"target.uniqueId": {"$binary": {"base64": "cl1YXk/xTxisymrFODZAgA==", "subType": "04"}}}]} + "{\"$and\": [{\"target.type\": \"USER\"}, {\"target.uniqueId\": {\"$binary\": {\"base64\": \"cl1YXk/xTxisymrFODZAgA==\", \"subType\": \"04\"}}}]}" + ), + Arguments.of( + ActionFilters.group("test"), + // {"$and": [{"target.type": "GROUP"}, {"target.name": "test"}]} + "{\"$and\": [{\"target.type\": \"GROUP\"}, {\"target.name\": \"test\"}]}" + ), + Arguments.of( + ActionFilters.track("test"), + // {"$and": [{"target.type": "TRACK"}, {"target.name": "test"}]} + "{\"$and\": [{\"target.type\": \"TRACK\"}, {\"target.name\": \"test\"}]}" + ), + Arguments.of( + ActionFilters.search("test"), + // {"$or": [{"source.name": {"$regularExpression": {"pattern": ".*test.*", "options": "i"}}}, {"target.name": {"$regularExpression": {"pattern": ".*test.*", "options": "i"}}}, {"description": {"$regularExpression": {"pattern": ".*test.*", "options": "i"}}}]} + "{\"$or\": [{\"source.name\": {\"$regularExpression\": {\"pattern\": \".*test.*\", \"options\": \"i\"}}}, {\"target.name\": {\"$regularExpression\": {\"pattern\": \".*test.*\", \"options\": \"i\"}}}, {\"description\": {\"$regularExpression\": {\"pattern\": \".*test.*\", \"options\": \"i\"}}}]}" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testQueries(FilterList filters, String expectedQuery) { + Bson bson = ActionFilterMongoBuilder.INSTANCE.make(filters); + + CodecRegistry codec = CodecRegistries.withUuidRepresentation(Bson.DEFAULT_CODEC_REGISTRY, UuidRepresentation.STANDARD); + String json = bson.toBsonDocument(BsonDocument.class, codec).toJson(); + + assertEquals(expectedQuery, json); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterSqlTest.java b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterSqlTest.java new file mode 100644 index 000000000..d5de2e342 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterSqlTest.java @@ -0,0 +1,83 @@ +/* + * 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.actionlog; + +import me.lucko.luckperms.common.actionlog.filter.ActionFilterSqlBuilder; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ActionFilterSqlTest { + + private static Stream testFiltersSql() { + return Stream.of( + Arguments.of( + ActionFilters.source(UUID.fromString("725d585e-4ff1-4f18-acca-6ac538364080")), + "WHERE actor_uuid = 725d585e-4ff1-4f18-acca-6ac538364080", + "WHERE actor_uuid = ?" + ), + Arguments.of( + ActionFilters.user(UUID.fromString("725d585e-4ff1-4f18-acca-6ac538364080")), + "WHERE type = U AND acted_uuid = 725d585e-4ff1-4f18-acca-6ac538364080", + "WHERE type = ? AND acted_uuid = ?" + ), + Arguments.of( + ActionFilters.group("test"), + "WHERE type = G AND acted_name = test", + "WHERE type = ? AND acted_name = ?" + ), + Arguments.of( + ActionFilters.track("test"), + "WHERE type = T AND acted_name = test", + "WHERE type = ? AND acted_name = ?" + ), + Arguments.of( + ActionFilters.search("test"), + "WHERE actor_name LIKE %test% OR acted_name LIKE %test% OR action LIKE %test%", + "WHERE actor_name LIKE ? OR acted_name LIKE ? OR action LIKE ?" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource + public void testFiltersSql(FilterList filters, String expectedSql, String expectedSqlParams) { + ActionFilterSqlBuilder sqlBuilder = new ActionFilterSqlBuilder(); + sqlBuilder.visit(filters); + + assertEquals(" " + expectedSql, sqlBuilder.builder().toReadableString()); + assertEquals(" " + expectedSqlParams, sqlBuilder.builder().toQueryString()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterTest.java b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterTest.java new file mode 100644 index 000000000..3fec1d6be --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/actionlog/ActionFilterTest.java @@ -0,0 +1,226 @@ +/* + * 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.actionlog; + +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +import me.lucko.luckperms.common.filter.FilterList; +import net.luckperms.api.actionlog.Action; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ActionFilterTest { + + @Test + public void testSource() { + UUID uuid = UUID.randomUUID(); + FilterList filter = ActionFilters.source(uuid); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(uuid) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + } + + @Test + public void testUser() { + UUID uuid = UUID.randomUUID(); + FilterList filter = ActionFilters.user(uuid); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .target(uuid) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(uuid) + .targetName("Test Target") + .description("test 123") + .build()) + ); + } + + @Test + public void testGroup() { + String name = "test"; + FilterList filter = ActionFilters.group(name); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName("aaaaa") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.TRACK) + .targetName(name) + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName(name) + .description("test 123") + .build()) + ); + } + + @Test + public void testTrack() { + String name = "test"; + FilterList filter = ActionFilters.track(name); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(UUID.randomUUID()) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.TRACK) + .targetName("aaaaa") + .description("test 123") + .build()) + ); + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName(name) + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.TRACK) + .targetName(name) + .description("test 123") + .build()) + ); + } + + @Test + public void testSearch() { + FilterList filter = ActionFilters.search("bar"); + + assertFalse(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName("Test Target") + .description("test 123") + .build()) + ); + + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("foobarbaz") + .targetType(Action.Target.Type.GROUP) + .targetName("Test Target") + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName("foobarbaz") + .description("test 123") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName("Test Target") + .description("foo bar baz") + .build()) + ); + assertTrue(filter.evaluate(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("bar") + .targetType(Action.Target.Type.GROUP) + .targetName("bar") + .description("bar") + .build()) + ); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlTest.java b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlTest.java new file mode 100644 index 000000000..062c93d42 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateSqlTest.java @@ -0,0 +1,143 @@ +/* + * 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.bulkupdate; + +import me.lucko.luckperms.common.bulkupdate.action.BulkUpdateAction; +import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; +import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; +import me.lucko.luckperms.common.filter.Comparison; +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; + +public class BulkUpdateSqlTest { + + private static Stream testSimpleActionSql() { + return Stream.of( + Arguments.of("DELETE FROM {table}", DeleteAction.create()), + Arguments.of("UPDATE {table} SET permission=foo", UpdateAction.of(BulkUpdateField.PERMISSION, "foo")), + Arguments.of("UPDATE {table} SET server=foo", UpdateAction.of(BulkUpdateField.SERVER, "foo")), + Arguments.of("UPDATE {table} SET world=foo", UpdateAction.of(BulkUpdateField.WORLD, "foo")) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testSimpleActionSql(String expectedSql, BulkUpdateAction action) { + BulkUpdate update = BulkUpdateBuilder.create() + .action(action) + .build(); + + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(update); + String sql = sqlBuilder.builder().toReadableString(); + + assertEquals(expectedSql, sql); + } + + private static Stream testQueryFilterSql() { + return Stream.of( + Arguments.of( + "DELETE FROM {table} WHERE permission = foo", + DeleteAction.create(), + BulkUpdateField.PERMISSION, + Comparison.EQUAL, + "foo" + ), + Arguments.of( + "DELETE FROM {table} WHERE permission != foo", + DeleteAction.create(), + BulkUpdateField.PERMISSION, + Comparison.NOT_EQUAL, + "foo" + ), + Arguments.of( + "DELETE FROM {table} WHERE permission LIKE foo", + DeleteAction.create(), + BulkUpdateField.PERMISSION, + Comparison.SIMILAR, + "foo" + ), + Arguments.of( + "DELETE FROM {table} WHERE permission NOT LIKE foo", + DeleteAction.create(), + BulkUpdateField.PERMISSION, + Comparison.NOT_SIMILAR, + "foo" + ), + Arguments.of( + "UPDATE {table} SET server=foo WHERE world = bar", + UpdateAction.of(BulkUpdateField.SERVER, "foo"), + BulkUpdateField.WORLD, + Comparison.EQUAL, + "bar" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testQueryFilterSql(String expectedSql, BulkUpdateAction action, BulkUpdateField field, Comparison comparison, String value) { + BulkUpdate update = BulkUpdateBuilder.create() + .action(action) + .filter(field, comparison, value) + .build(); + + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(update); + String sql = sqlBuilder.builder().toReadableString(); + + assertEquals(expectedSql, sql); + } + + @Test + public void testQueryFilterMultipleSql() { + BulkUpdate update = BulkUpdateBuilder.create() + .action(UpdateAction.of(BulkUpdateField.SERVER, "foo")) + .filter(BulkUpdateField.WORLD, Comparison.EQUAL, "bar") + .filter(BulkUpdateField.PERMISSION, Comparison.SIMILAR, "baz") + .filter(BulkUpdateField.SERVER, Comparison.NOT_EQUAL, "aaa") + .filter(BulkUpdateField.WORLD, Comparison.NOT_SIMILAR, "bbb") + .build(); + + BulkUpdateSqlBuilder sqlBuilder = new BulkUpdateSqlBuilder(); + sqlBuilder.visit(update); + assertEquals( + "UPDATE {table} SET server=? WHERE world = ? AND permission LIKE ? AND server != ? AND world NOT LIKE ?", + sqlBuilder.builder().toQueryString() + ); + assertEquals( + "UPDATE {table} SET server=foo WHERE world = bar AND permission LIKE baz AND server != aaa AND world NOT LIKE bbb", + sqlBuilder.builder().toReadableString() + ); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java new file mode 100644 index 000000000..ad8bc001c --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/bulkupdate/BulkUpdateTest.java @@ -0,0 +1,110 @@ +/* + * 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.bulkupdate; + +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.bulkupdate.action.DeleteAction; +import me.lucko.luckperms.common.bulkupdate.action.UpdateAction; +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.model.HolderType; +import me.lucko.luckperms.common.node.types.Permission; +import net.luckperms.api.node.Node; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BulkUpdateTest { + + @Test + public void testUpdate() { + BulkUpdate update = BulkUpdateBuilder.create() + .action(UpdateAction.of(BulkUpdateField.SERVER, "foo")) + .filter(BulkUpdateField.WORLD, Comparison.EQUAL, "bar") + .filter(BulkUpdateField.PERMISSION, Comparison.SIMILAR, "hello%") + .trackStatistics(true) + .build(); + + Instant time = Instant.now().plus(1, ChronoUnit.HOURS); + + Set nodes = ImmutableSet.of( + Permission.builder().permission("test").build(), + Permission.builder().permission("hello").build(), + Permission.builder().permission("hello").withContext("world", "bar").build(), + Permission.builder().permission("hello.world").value(false).expiry(time).withContext("world", "bar").build(), + Permission.builder().permission("hello").withContext("world", "bar").withContext("server", "bar").build() + ); + Set expected = ImmutableSet.of( + Permission.builder().permission("test").build(), + Permission.builder().permission("hello").build(), + Permission.builder().permission("hello").withContext("world", "bar").withContext("server", "foo").build(), + Permission.builder().permission("hello.world").value(false).expiry(time).withContext("world", "bar").withContext("server", "foo").build(), + Permission.builder().permission("hello").withContext("world", "bar").withContext("server", "foo").build() + ); + + assertEquals(expected, update.apply(nodes, HolderType.USER)); + + BulkUpdateStatistics statistics = update.getStatistics(); + assertEquals(3, statistics.getAffectedNodes()); + assertEquals(1, statistics.getAffectedUsers()); + assertEquals(0, statistics.getAffectedGroups()); + } + + @Test + public void testDelete() { + BulkUpdate update = BulkUpdateBuilder.create() + .action(DeleteAction.create()) + .filter(BulkUpdateField.WORLD, Comparison.EQUAL, "bar") + .filter(BulkUpdateField.PERMISSION, Comparison.SIMILAR, "hello%") + .trackStatistics(true) + .build(); + + Instant time = Instant.now().plus(1, ChronoUnit.HOURS); + + Set nodes = ImmutableSet.of( + Permission.builder().permission("test").build(), + Permission.builder().permission("hello").build(), + Permission.builder().permission("hello").withContext("world", "bar").build(), + Permission.builder().permission("hello.world").value(false).expiry(time).withContext("world", "bar").build(), + Permission.builder().permission("hello").withContext("world", "bar").withContext("server", "bar").build() + ); + Set expected = ImmutableSet.of( + Permission.builder().permission("test").build(), + Permission.builder().permission("hello").build() + ); + + assertEquals(expected, update.apply(nodes, HolderType.USER)); + + BulkUpdateStatistics statistics = update.getStatistics(); + assertEquals(3, statistics.getAffectedNodes()); + assertEquals(1, statistics.getAffectedUsers()); + assertEquals(0, statistics.getAffectedGroups()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaAccumulatorTest.java b/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaAccumulatorTest.java new file mode 100644 index 000000000..90b22c7e6 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaAccumulatorTest.java @@ -0,0 +1,168 @@ +/* + * 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.cacheddata; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import me.lucko.luckperms.common.cacheddata.metastack.SimpleMetaStackDefinition; +import me.lucko.luckperms.common.cacheddata.metastack.StandardStackElements; +import me.lucko.luckperms.common.cacheddata.result.IntegerResult; +import me.lucko.luckperms.common.cacheddata.result.StringResult; +import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator; +import me.lucko.luckperms.common.node.types.DisplayName; +import me.lucko.luckperms.common.node.types.Meta; +import me.lucko.luckperms.common.node.types.Prefix; +import me.lucko.luckperms.common.node.types.Suffix; +import me.lucko.luckperms.common.node.types.Weight; +import net.luckperms.api.metastacking.DuplicateRemovalFunction; +import net.luckperms.api.metastacking.MetaStackDefinition; +import net.luckperms.api.node.ChatMetaType; +import net.luckperms.api.node.types.MetaNode; +import net.luckperms.api.node.types.PrefixNode; +import net.luckperms.api.node.types.SuffixNode; +import net.luckperms.api.node.types.WeightNode; +import org.junit.jupiter.api.Test; + +import java.util.SortedMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MetaAccumulatorTest { + + @Test + public void testStates() { + SimpleMetaStackDefinition definition = new SimpleMetaStackDefinition(ImmutableList.of(StandardStackElements.HIGHEST), DuplicateRemovalFunction.RETAIN_ALL, "", "", ""); + MetaAccumulator accumulator = new MetaAccumulator(definition, definition); + + assertThrows(IllegalStateException.class, accumulator::getMeta); + assertThrows(IllegalStateException.class, () -> accumulator.getChatMeta(ChatMetaType.PREFIX)); + assertThrows(IllegalStateException.class, accumulator::getPrefixes); + assertThrows(IllegalStateException.class, accumulator::getSuffixes); + assertThrows(IllegalStateException.class, accumulator::getWeight); + assertThrows(IllegalStateException.class, accumulator::getPrimaryGroup); + assertThrows(IllegalStateException.class, accumulator::getPrefixDefinition); + assertThrows(IllegalStateException.class, accumulator::getSuffixDefinition); + assertThrows(IllegalStateException.class, accumulator::getPrefix); + assertThrows(IllegalStateException.class, accumulator::getSuffix); + + Prefix prefixNode = Prefix.builder("hello", 100).build(); + + accumulator.accumulateNode(prefixNode); + accumulator.complete(); + + assertThrows(IllegalStateException.class, () -> accumulator.accumulateNode(prefixNode)); + + StringResult prefixResult = accumulator.getPrefix(); + assertEquals(prefixNode, prefixResult.node()); + } + + @Test + public void testEmpty() { + SimpleMetaStackDefinition definition = new SimpleMetaStackDefinition(ImmutableList.of(StandardStackElements.HIGHEST), DuplicateRemovalFunction.RETAIN_ALL, "[", "|", "]"); + MetaAccumulator accumulator = new MetaAccumulator(definition, definition); + accumulator.complete(); + + ListMultimap> meta = accumulator.getMeta(); + assertEquals(0, meta.size()); + + SortedMap> prefixes = accumulator.getPrefixes(); + assertEquals(0, prefixes.size()); + + SortedMap> suffixes = accumulator.getSuffixes(); + assertEquals(0, suffixes.size()); + + IntegerResult weight = accumulator.getWeight(); + assertTrue(weight.isNull()); + assertEquals(0, weight.intResult()); + assertNull(weight.node()); + + String primaryGroup = accumulator.getPrimaryGroup(); + assertNull(primaryGroup); + + MetaStackDefinition prefixDefinition = accumulator.getPrefixDefinition(); + assertSame(definition, prefixDefinition); + + MetaStackDefinition suffixDefinition = accumulator.getSuffixDefinition(); + assertSame(definition, suffixDefinition); + + StringResult prefix = accumulator.getPrefix(); + assertNull(prefix.result()); + assertNull(prefix.node()); + + StringResult suffix = accumulator.getSuffix(); + assertNull(suffix.result()); + assertNull(suffix.node()); + } + + @Test + public void testSimple() { + SimpleMetaStackDefinition definition = new SimpleMetaStackDefinition(ImmutableList.of(StandardStackElements.HIGHEST), DuplicateRemovalFunction.RETAIN_ALL, "[", "|", "]"); + MetaAccumulator accumulator = new MetaAccumulator(definition, definition); + + accumulator.accumulateNode(Prefix.builder("b", 90).build()); + accumulator.accumulateNode(Prefix.builder("a", 100).build()); + accumulator.accumulateNode(Prefix.builder("c", 80).build()); + accumulator.accumulateNode(Suffix.builder("foo", 80).build()); + accumulator.accumulateNode(Meta.builder().key("foo").value("bar").build()); + accumulator.accumulateNode(Weight.builder(10).build()); // ignored + accumulator.accumulateNode(DisplayName.builder("hello").build()); + accumulator.accumulateWeight(IntegerResult.of(Weight.builder(5).build())); + accumulator.setPrimaryGroup("member"); + + accumulator.complete(); + + StringResult prefix = accumulator.getPrefix(); + assertEquals("[a]", prefix.result()); + + SortedMap> prefixes = accumulator.getPrefixes(); + assertEquals(ImmutableSet.of(100, 90, 80), prefixes.keySet()); + + StringResult suffix = accumulator.getSuffix(); + assertEquals("[foo]", suffix.result()); + + SortedMap> suffixes = accumulator.getSuffixes(); + assertEquals(ImmutableSet.of(80), suffixes.keySet()); + + ListMultimap> meta = accumulator.getMeta(); + assertEquals(3, meta.size()); + assertEquals(ImmutableSet.of("foo", "weight", "primarygroup"), meta.keySet()); + assertEquals("bar", meta.get("foo").get(0).result()); + assertEquals("5", meta.get("weight").get(0).result()); + assertEquals("member", meta.get("primarygroup").get(0).result()); + + IntegerResult weight = accumulator.getWeight(); + assertEquals(5, weight.intResult()); + + String primaryGroup = accumulator.getPrimaryGroup(); + assertEquals("member", primaryGroup); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaStackAccumulatorTest.java b/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaStackAccumulatorTest.java new file mode 100644 index 000000000..f21b80122 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaStackAccumulatorTest.java @@ -0,0 +1,104 @@ +/* + * 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.cacheddata; + +import com.google.common.collect.ImmutableList; +import me.lucko.luckperms.common.cacheddata.metastack.SimpleMetaStackDefinition; +import me.lucko.luckperms.common.cacheddata.metastack.StandardStackElements; +import me.lucko.luckperms.common.cacheddata.result.StringResult; +import me.lucko.luckperms.common.cacheddata.type.MetaStackAccumulator; +import me.lucko.luckperms.common.node.types.Prefix; +import net.luckperms.api.metastacking.DuplicateRemovalFunction; +import net.luckperms.api.node.ChatMetaType; +import net.luckperms.api.node.types.PrefixNode; +import org.junit.jupiter.api.Test; + +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 MetaStackAccumulatorTest { + + @Test + public void testEmpty() { + SimpleMetaStackDefinition definition = new SimpleMetaStackDefinition(ImmutableList.of(StandardStackElements.HIGHEST), DuplicateRemovalFunction.RETAIN_ALL, "", "", ""); + MetaStackAccumulator accumulator = new MetaStackAccumulator<>(definition, ChatMetaType.PREFIX); + + StringResult result = accumulator.toResult(); + assertNotNull(result); + assertNull(result.result()); + assertNull(result.node()); + assertNull(result.overriddenResult()); + + String formattedString = accumulator.toFormattedString(); + assertNull(formattedString); + } + + @Test + public void testSingle() { + SimpleMetaStackDefinition definition = new SimpleMetaStackDefinition(ImmutableList.of(StandardStackElements.HIGHEST), DuplicateRemovalFunction.RETAIN_ALL, "[", "|", "]"); + MetaStackAccumulator accumulator = new MetaStackAccumulator<>(definition, ChatMetaType.PREFIX); + + PrefixNode a = Prefix.builder("a", 100).build(); + PrefixNode b = Prefix.builder("b", 90).build(); + PrefixNode c = Prefix.builder("c", 80).build(); + + accumulator.offer(b); + accumulator.offer(a); + accumulator.offer(c); + + StringResult result = accumulator.toResult(); + assertNotNull(result); + assertEquals("[a]", result.result()); + assertEquals(a, result.node()); + assertNull(result.overriddenResult()); + } + + @Test + public void testMultiple() { + SimpleMetaStackDefinition definition = new SimpleMetaStackDefinition(ImmutableList.of(StandardStackElements.LOWEST, StandardStackElements.HIGHEST), DuplicateRemovalFunction.RETAIN_ALL, "[", "|", "]"); + MetaStackAccumulator accumulator = new MetaStackAccumulator<>(definition, ChatMetaType.PREFIX); + + PrefixNode a = Prefix.builder("a", 100).build(); + PrefixNode b = Prefix.builder("b", 90).build(); + PrefixNode c = Prefix.builder("c", 80).build(); + + accumulator.offer(b); + accumulator.offer(a); + accumulator.offer(c); + + StringResult result = accumulator.toResult(); + assertNotNull(result); + assertEquals("[c|a]", result.result()); + assertEquals(c, result.node()); + + StringResult overriddenResult = result.overriddenResult(); + assertNotNull(overriddenResult); + assertEquals(a, overriddenResult.node()); + assertNull(overriddenResult.overriddenResult()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaStackElementTest.java b/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaStackElementTest.java new file mode 100644 index 000000000..bbd79bb69 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaStackElementTest.java @@ -0,0 +1,243 @@ +/* + * 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.cacheddata; + +import com.google.common.collect.ImmutableList; +import me.lucko.luckperms.common.cacheddata.metastack.StandardStackElements; +import me.lucko.luckperms.common.model.HolderType; +import me.lucko.luckperms.common.model.InheritanceOrigin; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; +import me.lucko.luckperms.common.model.Track; +import me.lucko.luckperms.common.model.manager.track.StandardTrackManager; +import me.lucko.luckperms.common.model.manager.track.TrackManager; +import me.lucko.luckperms.common.node.types.Prefix; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import net.luckperms.api.metastacking.MetaStackElement; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.ChatMetaType; +import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MetaStackElementTest { + + @Test + public void testHighest() { + MetaStackElement highest = StandardStackElements.HIGHEST; + assertTrue(highest.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).build(), // node + null // current + )); + assertFalse(highest.shouldAccumulate( + ChatMetaType.SUFFIX, + Prefix.builder("foo", 100).build(), // node + null // current + )); + assertTrue(highest.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).build(), // node + Prefix.builder("bar", 50).build()) // current + ); + assertFalse(highest.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 50).build(), // node + Prefix.builder("bar", 100).build()) // current + ); + } + + @Test + public void testLowest() { + MetaStackElement lowest = StandardStackElements.LOWEST; + assertTrue(lowest.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).build(), // node + null // current + )); + assertFalse(lowest.shouldAccumulate( + ChatMetaType.SUFFIX, + Prefix.builder("foo", 100).build(), // node + null // current + )); + assertTrue(lowest.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 50).build(), // node + Prefix.builder("bar", 100).build()) // current + ); + assertFalse(lowest.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).build(), // node + Prefix.builder("bar", 50).build()) // current + ); + } + + @Test + public void testHighestOwn() { + InheritanceOrigin userOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.USER, ""), DataType.NORMAL); + InheritanceOrigin groupOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, ""), DataType.NORMAL); + + MetaStackElement highestOwn = StandardStackElements.HIGHEST_OWN; + assertTrue(highestOwn.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, userOrigin).build(), // node + null // current + )); + assertFalse(highestOwn.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, groupOrigin).build(), // node + null // current + )); + } + + @Test + public void testHighestInherited() { + InheritanceOrigin userOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.USER, ""), DataType.NORMAL); + InheritanceOrigin groupOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, ""), DataType.NORMAL); + + MetaStackElement highestInherited = StandardStackElements.HIGHEST_INHERITED; + assertTrue(highestInherited.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, groupOrigin).build(), // node + null // current + )); + assertFalse(highestInherited.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, userOrigin).build(), // node + null // current + )); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void testHighestFromGroupOnTrack() { + LuckPermsPlugin plugin = mock(LuckPermsPlugin.class); + TrackManager trackManager = new StandardTrackManager(plugin); + when(plugin.getTrackManager()).thenReturn((TrackManager) trackManager); + + Track track = trackManager.getOrMake("test"); + track.setGroups(ImmutableList.of("foo", "bar")); + + InheritanceOrigin fooOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, "foo"), DataType.NORMAL); + InheritanceOrigin bazOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, "baz"), DataType.NORMAL); + + MetaStackElement highestFromGroupOnTrack = StandardStackElements.highestFromGroupOnTrack(plugin, "test"); + assertTrue(highestFromGroupOnTrack.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, fooOrigin).build(), // node + null // current + )); + assertFalse(highestFromGroupOnTrack.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, bazOrigin).build(), // node + null // current + )); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void testHighestNotFromGroupOnTrack() { + LuckPermsPlugin plugin = mock(LuckPermsPlugin.class); + TrackManager trackManager = new StandardTrackManager(plugin); + when(plugin.getTrackManager()).thenReturn((TrackManager) trackManager); + + Track track = trackManager.getOrMake("test"); + track.setGroups(ImmutableList.of("foo", "bar")); + + InheritanceOrigin fooOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, "foo"), DataType.NORMAL); + InheritanceOrigin bazOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, "baz"), DataType.NORMAL); + InheritanceOrigin userOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.USER, "foo"), DataType.NORMAL); + + MetaStackElement highestNotFromGroupOnTrack = StandardStackElements.highestNotFromGroupOnTrack(plugin, "test"); + assertTrue(highestNotFromGroupOnTrack.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, userOrigin).build(), // node + null // current + )); + assertTrue(highestNotFromGroupOnTrack.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, bazOrigin).build(), // node + null // current + )); + assertFalse(highestNotFromGroupOnTrack.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, fooOrigin).build(), // node + null // current + )); + } + + @Test + public void testHighestFromGroup() { + InheritanceOrigin fooOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, "foo"), DataType.NORMAL); + InheritanceOrigin bazOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, "baz"), DataType.NORMAL); + InheritanceOrigin userOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.USER, "foo"), DataType.NORMAL); + + MetaStackElement highestFromGroup = StandardStackElements.highestFromGroup("foo"); + assertTrue(highestFromGroup.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, fooOrigin).build(), // node + null // current + )); + assertFalse(highestFromGroup.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, bazOrigin).build(), // node + null // current + )); + assertFalse(highestFromGroup.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, userOrigin).build(), // node + null // current + )); + } + + @Test + public void testHighestNotFromGroup() { + InheritanceOrigin fooOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, "foo"), DataType.NORMAL); + InheritanceOrigin bazOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.GROUP, "baz"), DataType.NORMAL); + InheritanceOrigin userOrigin = new InheritanceOrigin(new PermissionHolderIdentifier(HolderType.USER, "foo"), DataType.NORMAL); + + MetaStackElement highestNotFromGroup = StandardStackElements.highestNotFromGroup("foo"); + assertTrue(highestNotFromGroup.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, userOrigin).build(), // node + null // current + )); + assertTrue(highestNotFromGroup.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, bazOrigin).build(), // node + null // current + )); + assertFalse(highestNotFromGroup.shouldAccumulate( + ChatMetaType.PREFIX, + Prefix.builder("foo", 100).withMetadata(InheritanceOriginMetadata.KEY, fooOrigin).build(), // node + null // current + )); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaValueSelectorTest.java b/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaValueSelectorTest.java new file mode 100644 index 000000000..c66f277ab --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/cacheddata/MetaValueSelectorTest.java @@ -0,0 +1,81 @@ +/* + * 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.cacheddata; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import me.lucko.luckperms.common.cacheddata.result.StringResult; +import me.lucko.luckperms.common.cacheddata.type.SimpleMetaValueSelector; +import me.lucko.luckperms.common.cacheddata.type.SimpleMetaValueSelector.Strategy; +import net.luckperms.api.cacheddata.Result; +import net.luckperms.api.node.types.MetaNode; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class MetaValueSelectorTest { + + @Test + public void testStrategies() { + Map strategies = ImmutableMap.of( + "foo", Strategy.HIGHEST_NUMBER, + "bar", Strategy.LOWEST_NUMBER + ); + SimpleMetaValueSelector selector = new SimpleMetaValueSelector(strategies, Strategy.INHERITANCE); + + // empty + assertThrows(IllegalArgumentException.class, () -> selector.selectValue("hello", ImmutableList.of())); + + Result foo = StringResult.of("foo"); + + // single value + Result value = selector.selectValue("abc", ImmutableList.of(foo)); + assertSame(foo, value); + + // fallback to default when values are not numbers + value = selector.selectValue("foo", ImmutableList.of(foo)); + assertSame(foo, value); + + Result one = StringResult.of("1"); + Result two = StringResult.of("2"); + Result three = StringResult.of("3"); + ImmutableList> values = ImmutableList.of(two, one, three); + + // first value + assertSame(two, selector.selectValue("abc", values)); + + // highest value + assertSame(three, selector.selectValue("foo", values)); + + // lowest value + assertSame(one, selector.selectValue("bar", values)); + + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/cacheddata/UsageTrackedTest.java b/common/src/test/java/me/lucko/luckperms/common/cacheddata/UsageTrackedTest.java new file mode 100644 index 000000000..74812b961 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/cacheddata/UsageTrackedTest.java @@ -0,0 +1,54 @@ +/* + * 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.cacheddata; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class UsageTrackedTest { + + @Test + public void testUsedRecently() { + TestUsageTracked usageTracked = new TestUsageTracked(); + assertTrue(usageTracked.usedInTheLast(1, TimeUnit.MINUTES)); + usageTracked.recordUsage(); + assertTrue(usageTracked.usedInTheLast(1, TimeUnit.MINUTES)); + + usageTracked.setLastUsed(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1)); + assertFalse(usageTracked.usedInTheLast(1, TimeUnit.MINUTES)); + } + + static final class TestUsageTracked extends UsageTracked { + void setLastUsed(long lastUsed) { + this.lastUsed = lastUsed; + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/calculator/PermissionCalculatorTest.java b/common/src/test/java/me/lucko/luckperms/common/calculator/PermissionCalculatorTest.java new file mode 100644 index 000000000..1ec1cac4e --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/calculator/PermissionCalculatorTest.java @@ -0,0 +1,226 @@ +/* + * 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.calculator; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractOverrideWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.DirectProcessor; +import me.lucko.luckperms.common.calculator.processor.RegexProcessor; +import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; +import me.lucko.luckperms.common.node.factory.NodeBuilders; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import net.luckperms.api.node.Node; +import net.luckperms.api.util.Tristate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class PermissionCalculatorTest { + + private static final Map EXAMPLE_PERMISSIONS = ImmutableMap.builder() + // direct + .put("test.node1", true) + .put("test.node2", false) + + // wildcard + .put("one.two.three.four", true) + .put("one.two.three.*", false) + .put("one.two.three", true) + .put("one.two.*", false) + .put("one.two", true) + .put("one.*", false) + .put("one", true) + .put("*", false) + + // regex + .put("r=hello\\d+", true) + .put("R=rege(x(es)?|xps?)[1-5]", false) + + // override + .put("overridetest.*", true) + + .build().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> NodeBuilders.determineMostApplicable(e.getKey()).value(e.getValue()).build() + )); + + @ParameterizedTest + @CsvSource({ + "test, UNDEFINED", + "test.node1, TRUE", + "test.node2, FALSE" + }) + public void testDirect(String node, Tristate expected) { + PermissionCalculator calculator = new PermissionCalculatorBase(ImmutableList.of( + new DirectProcessor(EXAMPLE_PERMISSIONS) + )); + + TristateResult result = calculator.checkPermission(node, CheckOrigin.INTERNAL); + assertEquals(expected, result.result()); + assertNull(result.overriddenResult()); + + if (expected != Tristate.UNDEFINED) { + assertNotNull(result.node()); + assertSame(DirectProcessor.class, result.processorClass()); + } else { + assertNull(result.node()); + assertNull(result.processorClass()); + } + } + + @ParameterizedTest + @CsvSource({ + "one.two.three.four, true, direct", + "one.two.three.test, false, wildcard", + "one.two.three.*, false, direct", + "one.two.three, true, direct", + "one.two.test, false, wildcard", + "one.two.*, false, direct", + "one.two, true, direct", + "one.test, false, wildcard", + "one.*, false, direct", + "one, true, direct", + "test, false, wildcard", + "*, false, direct", + }) + public void testWildcard(String node, boolean expected, String type) { + PermissionCalculator calculator = new PermissionCalculatorBase(ImmutableList.of( + new DirectProcessor(EXAMPLE_PERMISSIONS), + new WildcardProcessor(EXAMPLE_PERMISSIONS) + )); + + TristateResult result = calculator.checkPermission(node, CheckOrigin.INTERNAL); + assertEquals(Tristate.of(expected), result.result()); + assertNull(result.overriddenResult()); + assertNotNull(result.node()); + + if (type.equals("direct")) { + assertSame(DirectProcessor.class, result.processorClass()); + } else if (type.equals("wildcard")) { + assertSame(WildcardProcessor.class, result.processorClass()); + } else { + throw new AssertionError(); + } + } + + @ParameterizedTest + @CsvSource({ + "one, true, direct", + "one.test, true, wildcard", + "one.two, true, direct", + "one.two.test, true, wildcard", + }) + public void testSpongeWildcard(String node, boolean expected, String type) { + PermissionCalculator calculator = new PermissionCalculatorBase(ImmutableList.of( + new DirectProcessor(EXAMPLE_PERMISSIONS), + new SpongeWildcardProcessor(EXAMPLE_PERMISSIONS) + )); + + TristateResult result = calculator.checkPermission(node, CheckOrigin.INTERNAL); + assertEquals(Tristate.of(expected), result.result()); + assertNull(result.overriddenResult()); + assertNotNull(result.node()); + + if (type.equals("direct")) { + assertSame(DirectProcessor.class, result.processorClass()); + } else if (type.equals("wildcard")) { + assertSame(SpongeWildcardProcessor.class, result.processorClass()); + } else { + throw new AssertionError(); + } + } + + @ParameterizedTest + @CsvSource({ + "hello, UNDEFINED", + "hello1, TRUE", + "hello123, TRUE", + "helloo, UNDEFINED", + "regex1, FALSE", + "regexes2, FALSE", + "regexp3, FALSE", + "regexps4, FALSE", + }) + public void testRegex(String node, Tristate expected) { + PermissionCalculator calculator = new PermissionCalculatorBase(ImmutableList.of( + new DirectProcessor(EXAMPLE_PERMISSIONS), + new RegexProcessor(EXAMPLE_PERMISSIONS) + )); + + TristateResult result = calculator.checkPermission(node, CheckOrigin.INTERNAL); + assertEquals(expected, result.result()); + assertNull(result.overriddenResult()); + + if (expected != Tristate.UNDEFINED) { + assertNotNull(result.node()); + assertSame(RegexProcessor.class, result.processorClass()); + } else { + assertNull(result.node()); + assertNull(result.processorClass()); + } + } + + @Test + public void testOverrideWildcard() { + AbstractOverrideWildcardProcessor overrideProcessor = new AbstractOverrideWildcardProcessor(true) { + @Override + protected TristateResult hasPermission(String permission) { + if (permission.equals("overridetest.test")) { + return new TristateResult.Factory(AbstractOverrideWildcardProcessor.class) + .result(Tristate.FALSE); + } + return TristateResult.UNDEFINED; + } + }; + + PermissionCalculator calculator = new PermissionCalculatorBase(ImmutableList.of( + new DirectProcessor(EXAMPLE_PERMISSIONS), + new WildcardProcessor(EXAMPLE_PERMISSIONS), + overrideProcessor + )); + + TristateResult result = calculator.checkPermission("overridetest.test", CheckOrigin.INTERNAL); + assertEquals(Tristate.FALSE, result.result()); + assertSame(AbstractOverrideWildcardProcessor.class, result.processorClass()); + + TristateResult overriddenResult = result.overriddenResult(); + assertNotNull(overriddenResult); + assertSame(WildcardProcessor.class, overriddenResult.processorClass()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/calculator/PermissionProcessorTest.java b/common/src/test/java/me/lucko/luckperms/common/calculator/PermissionProcessorTest.java new file mode 100644 index 000000000..82cc609b8 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/calculator/PermissionProcessorTest.java @@ -0,0 +1,165 @@ +/* + * 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.calculator; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.DirectProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.RegexProcessor; +import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; +import me.lucko.luckperms.common.node.factory.NodeBuilders; +import net.luckperms.api.node.Node; +import net.luckperms.api.util.Tristate; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class PermissionProcessorTest { + + @ParameterizedTest + @CsvSource({ + "test, UNDEFINED", + "test.node1, TRUE", + "test.node2, FALSE" + }) + public void testDirect(String node, Tristate expected) { + PermissionProcessor processor = new DirectProcessor(createNodeMap(Map.of( + "test.node1", true, + "test.node2", false + ))); + + TristateResult result = processor.hasPermission(TristateResult.UNDEFINED, node); + assertEquals(expected, result.result()); + assertNull(result.overriddenResult()); + + if (expected != Tristate.UNDEFINED) { + assertNotNull(result.node()); + assertSame(DirectProcessor.class, result.processorClass()); + } else { + assertNull(result.node()); + assertNull(result.processorClass()); + } + } + + @ParameterizedTest + @CsvSource({ + "one.two.three.four, TRUE", + "one.two.test, TRUE", + "one.two, FALSE", + "one.test, FALSE", + "one.*, FALSE", + "one, TRUE", + "test, TRUE", + "*, TRUE", + }) + public void testWildcard(String node, Tristate expected) { + PermissionProcessor processor = new WildcardProcessor(createNodeMap(Map.of( + "one.two.*", true, + "one.*", false, + "*", true + ))); + + TristateResult result = processor.hasPermission(TristateResult.UNDEFINED, node); + assertEquals(expected, result.result()); + assertNull(result.overriddenResult()); + assertNotNull(result.node()); + assertSame(WildcardProcessor.class, result.processorClass()); + } + + @ParameterizedTest + @CsvSource({ + "one.two.three.test, FALSE", + "one.two.three, TRUE", + "one.two.test, TRUE", + "one.two, FALSE", + "one.test, FALSE", + "one, UNDEFINED", + }) + public void testSpongeWildcard(String node, Tristate expected) { + PermissionProcessor processor = new SpongeWildcardProcessor(createNodeMap(Map.of( + "one.two.three", false, + "one.two", true, + "one", false + ))); + + TristateResult result = processor.hasPermission(TristateResult.UNDEFINED, node); + assertEquals(expected, result.result()); + + if (expected != Tristate.UNDEFINED) { + assertNotNull(result.node()); + assertSame(SpongeWildcardProcessor.class, result.processorClass()); + } else { + assertNull(result.node()); + assertNull(result.processorClass()); + } + } + + @ParameterizedTest + @CsvSource({ + "hello, UNDEFINED", + "hello1, TRUE", + "hello123, TRUE", + "helloo, UNDEFINED", + "regex1, FALSE", + "regexes2, FALSE", + "regexp3, FALSE", + "regexps4, FALSE", + }) + public void testRegex(String node, Tristate expected) { + PermissionProcessor processor = new RegexProcessor(createNodeMap(Map.of( + "r=hello\\d+", true, + "R=rege(x(es)?|xps?)[1-5]", false + ))); + + TristateResult result = processor.hasPermission(TristateResult.UNDEFINED, node); + assertEquals(expected, result.result()); + assertNull(result.overriddenResult()); + + if (expected != Tristate.UNDEFINED) { + assertNotNull(result.node()); + assertSame(RegexProcessor.class, result.processorClass()); + } else { + assertNull(result.node()); + assertNull(result.processorClass()); + } + } + + private static Map createNodeMap(Map nodes) { + return nodes.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + e -> NodeBuilders.determineMostApplicable(e.getKey()).value(e.getValue()).build() + )); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/command/access/CommandPermissionTest.java b/common/src/test/java/me/lucko/luckperms/common/command/access/CommandPermissionTest.java new file mode 100644 index 000000000..27a544db9 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/command/access/CommandPermissionTest.java @@ -0,0 +1,83 @@ +/* + * 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.access; + +import com.google.common.collect.ImmutableSet; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CommandPermissionTest { + + private static final ImmutableSet ALLOWED_READ_ONLY = ImmutableSet.of( + CommandPermission.SYNC, + CommandPermission.INFO, + CommandPermission.EDITOR, + CommandPermission.VERBOSE, + CommandPermission.TREE, + CommandPermission.SEARCH, + CommandPermission.EXPORT, + CommandPermission.RELOAD_CONFIG, + CommandPermission.TRANSLATIONS, + CommandPermission.LIST_GROUPS, + CommandPermission.LIST_TRACKS, + CommandPermission.USER_INFO, + CommandPermission.USER_PERM_INFO, + CommandPermission.USER_PERM_CHECK, + CommandPermission.USER_PARENT_INFO, + CommandPermission.USER_META_INFO, + CommandPermission.USER_EDITOR, + CommandPermission.USER_SHOW_TRACKS, + CommandPermission.GROUP_INFO, + CommandPermission.GROUP_PERM_INFO, + CommandPermission.GROUP_PERM_CHECK, + CommandPermission.GROUP_PARENT_INFO, + CommandPermission.GROUP_META_INFO, + CommandPermission.GROUP_EDITOR, + CommandPermission.GROUP_LIST_MEMBERS, + CommandPermission.GROUP_SHOW_TRACKS, + CommandPermission.TRACK_INFO, + CommandPermission.TRACK_EDITOR, + CommandPermission.LOG_RECENT, + CommandPermission.LOG_USER_HISTORY, + CommandPermission.LOG_GROUP_HISTORY, + CommandPermission.LOG_TRACK_HISTORY, + CommandPermission.LOG_SEARCH, + CommandPermission.LOG_NOTIFY, + CommandPermission.SPONGE_PERMISSION_INFO, + CommandPermission.SPONGE_PARENT_INFO, + CommandPermission.SPONGE_OPTION_INFO + ); + + @ParameterizedTest + @EnumSource(CommandPermission.class) + public void testReadOnly(CommandPermission permission) { + String name = permission.name(); + assertEquals(ALLOWED_READ_ONLY.contains(permission), permission.isReadOnly(), name); + } + +} 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/common/src/test/java/me/lucko/luckperms/common/config/FileSecretConfigAdapterTest.java b/common/src/test/java/me/lucko/luckperms/common/config/FileSecretConfigAdapterTest.java new file mode 100644 index 000000000..5cb0677cd --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/config/FileSecretConfigAdapterTest.java @@ -0,0 +1,76 @@ +/* + * 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.config; + +import me.lucko.luckperms.common.config.generic.adapter.FileSecretConfigAdapter; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.lenient; + +@ExtendWith(MockitoExtension.class) +public class FileSecretConfigAdapterTest { + + @Mock private LuckPermsPlugin plugin; + @Mock private PluginLogger logger; + + @BeforeEach + public void setupMocks() { + lenient().when(this.plugin.getLogger()).thenReturn(this.logger); + } + + @Test + public void testRead(@TempDir Path directory) throws IOException { + Path path = directory.resolve("luckperms_server"); + Files.write(path, "test".getBytes()); + + FileSecretConfigAdapter adapter = new FileSecretConfigAdapter(this.plugin, directory.toString()); + + String server = adapter.getString("server", null); + assertEquals("test", server); + } + + @Test + public void testNoOp() throws IOException { + FileSecretConfigAdapter adapter = new FileSecretConfigAdapter(this.plugin, null); + + String server = adapter.getString("server", null); + assertNull(server); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/context/ContextSetComparatorTest.java b/common/src/test/java/me/lucko/luckperms/common/context/ContextSetComparatorTest.java new file mode 100644 index 000000000..2cd3ef489 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/context/ContextSetComparatorTest.java @@ -0,0 +1,175 @@ +/* + * 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.context; + +import com.google.common.collect.Lists; +import me.lucko.luckperms.common.context.comparator.ContextSetComparator; +import net.luckperms.api.context.ImmutableContextSet; +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.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ContextSetComparatorTest { + + private static final ImmutableContextSet EMPTY = ImmutableContextSetImpl.EMPTY; + private static final ImmutableContextSet JUST_SERVER = ImmutableContextSetImpl.of("server", "foo"); + private static final ImmutableContextSet JUST_WORLD = ImmutableContextSetImpl.of("world", "foo"); + private static final ImmutableContextSet SERVER_AND_WORLD = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("world", "foo") + .build(); + private static final ImmutableContextSet MISC = new ImmutableContextSetImpl.BuilderImpl() + .add("foo", "foo") + .add("foo", "bar") + .build(); + private static final ImmutableContextSet MISC_2 = new ImmutableContextSetImpl.BuilderImpl() + .add("foo", "foo") + .add("foo", "bar") + .add("bar", "foo") + .build(); + private static final ImmutableContextSet SERVER_AND_MISC = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("foo", "foo") + .add("foo", "bar") + .build(); + private static final ImmutableContextSet WORLD_AND_MISC = new ImmutableContextSetImpl.BuilderImpl() + .add("world", "foo") + .add("foo", "foo") + .add("foo", "bar") + .build(); + private static final ImmutableContextSet SERVER_AND_WORLD_AND_MISC_1 = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("world", "foo") + .add("foo", "foo") + .build(); + private static final ImmutableContextSet SERVER_AND_WORLD_AND_MISC_2 = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("world", "foo") + .add("foo", "foo") + .add("foo", "bar") + .build(); + + private static Stream all() { + return Stream.of(EMPTY, JUST_SERVER, JUST_WORLD, SERVER_AND_WORLD, MISC, MISC_2, SERVER_AND_MISC, WORLD_AND_MISC, SERVER_AND_WORLD_AND_MISC_1, SERVER_AND_WORLD_AND_MISC_2); + } + + private static final Comparator ASCENDING = ContextSetComparator.ascending(); + + @ParameterizedTest + @MethodSource("all") + @SuppressWarnings("EqualsWithItself") + public void testEquals(ImmutableContextSet set) { + assertEquals(0, ASCENDING.compare(set, set)); + } + + @Test + public void testEmpty() { + assertTrue(ASCENDING.compare(JUST_SERVER, EMPTY) > 0); + assertTrue(ASCENDING.compare(EMPTY, JUST_SERVER) < 0); + } + + @Test + public void testServerPresence() { + assertTrue(ASCENDING.compare(JUST_SERVER, MISC) > 0); + assertTrue(ASCENDING.compare(JUST_SERVER, WORLD_AND_MISC) > 0); + } + + @Test + public void testWorldPresence() { + assertTrue(ASCENDING.compare(JUST_WORLD, MISC) > 0); + } + + @Test + public void testOverallSize() { + assertTrue(ASCENDING.compare(SERVER_AND_MISC, JUST_SERVER) > 0); + assertTrue(ASCENDING.compare(WORLD_AND_MISC, JUST_WORLD) > 0); + } + + @ParameterizedTest + @MethodSource("all") + public void testOverallSizeAll(ImmutableContextSet other) { + if (other == SERVER_AND_WORLD_AND_MISC_2) return; + assertTrue(ASCENDING.compare(SERVER_AND_WORLD_AND_MISC_2, other) > 0); + } + + private static Stream testTransitivity() { + return Stream.of( + Arguments.of( + ImmutableContextSetImpl.of("a", "-"), + ImmutableContextSetImpl.of("b", "-"), + ImmutableContextSetImpl.of("c", "-") + ), + Arguments.of( + ImmutableContextSetImpl.of("-", "a"), + ImmutableContextSetImpl.of("-", "b"), + ImmutableContextSetImpl.of("-", "c") + ) + ); + } + + @ParameterizedTest + @MethodSource + public void testTransitivity(ImmutableContextSet a, ImmutableContextSet b, ImmutableContextSet c) { + List list = new ArrayList<>(); + list.add(a); + list.add(b); + list.add(c); + list.sort(ASCENDING); + + List reversed = new ArrayList<>(list); + reversed.sort(ASCENDING.reversed()); + + assertEquals(Lists.reverse(list), reversed); + } + + @Test + public void testPriorityOrdering() { + List list = new ArrayList<>(); + list.add(EMPTY); + list.add(JUST_SERVER); + list.add(JUST_WORLD); + list.add(MISC); + list.add(MISC_2); + list.sort(ContextSetComparator.ascending()); + + assertEquals(List.of(EMPTY, MISC, MISC_2, JUST_WORLD, JUST_SERVER), list); + + // list sorted with the descending comparator should be exactly the reverse of the ascending list + List reversed = new ArrayList<>(list); + reversed.sort(ContextSetComparator.descending()); + assertEquals(Lists.reverse(list), reversed); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/context/ContextSetJsonSerializerTest.java b/common/src/test/java/me/lucko/luckperms/common/context/ContextSetJsonSerializerTest.java new file mode 100644 index 000000000..5202c5a7c --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/context/ContextSetJsonSerializerTest.java @@ -0,0 +1,96 @@ +/* + * 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.context; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import me.lucko.luckperms.common.context.serializer.ContextSetJsonSerializer; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.ImmutableContextSet; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ContextSetJsonSerializerTest { + + private static final Gson GSON = new Gson(); + + private static final ImmutableContextSet EXAMPLE_1 = new ImmutableContextSetImpl.BuilderImpl() + .add("server", "foo") + .add("world", "foo") + .add("foo", "foo") + .add("foo", "bar") + .build(); + + private static final ImmutableContextSet EXAMPLE_2 = new ImmutableContextSetImpl.BuilderImpl() + .add("cc", "foo") + .add("bb", "foo") + .add("aa", "foo") + .build(); + + @Test + public void testDeserialize() { + String string = "{\"foo\":[\"bar\",\"foo\"],\"server\":\"foo\",\"world\":[\"foo\"]}"; + ContextSet set = ContextSetJsonSerializer.deserialize(GSON, string); + assertEquals(EXAMPLE_1, set); + } + + @Test + public void testSerialize() { + JsonObject obj1 = ContextSetJsonSerializer.serialize(EXAMPLE_1); + assertEquals(3, obj1.size()); + assertEquals("{\"foo\":[\"bar\",\"foo\"],\"server\":\"foo\",\"world\":\"foo\"}", obj1.toString()); + + JsonObject obj2 = ContextSetJsonSerializer.serialize(EXAMPLE_2); + assertEquals(3, obj2.size()); + assertEquals("{\"aa\":\"foo\",\"bb\":\"foo\",\"cc\":\"foo\"}", obj2.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "{}", + "{ }", + "" + }) + public void testDeserializeEmpty(String json) { + assertEquals(ImmutableContextSetImpl.EMPTY, ContextSetJsonSerializer.deserialize(GSON, json)); + } + + @ParameterizedTest + @ValueSource(strings = { + "null", + "[]", + "foo" + }) + public void testDeserializeThrows(String json) { + assertThrows(JsonParseException.class, () -> ContextSetJsonSerializer.deserialize(GSON, json)); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/context/ImmutableContextSetTest.java b/common/src/test/java/me/lucko/luckperms/common/context/ImmutableContextSetTest.java new file mode 100644 index 000000000..e53867de7 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/context/ImmutableContextSetTest.java @@ -0,0 +1,173 @@ +/* + * 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.context; + +import com.google.common.collect.ImmutableSet; +import net.luckperms.api.context.Context; +import net.luckperms.api.context.ContextSatisfyMode; +import net.luckperms.api.context.ImmutableContextSet; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ImmutableContextSetTest { + + private static Stream> testBuilder() { + return Stream.of( + builder -> { + builder.add("test", "a"); + builder.add("test", "b"); + builder.add("test", "c"); + }, + builder -> { + builder.add("test", "c"); + builder.add("test", "b"); + builder.add("test", "a"); + }, + builder -> { + builder.add("test", "b"); + builder.add("test", "a"); + builder.add("test", "c"); + }, + builder -> { + builder.add("test", "b"); + builder.add("test", "c"); + builder.add("test", "a"); + }, + builder -> { + builder.add("test", "a"); + builder.add("test", "a"); + builder.add("test", "b"); + builder.add("test", "c"); + } + ); + } + + @ParameterizedTest + @MethodSource + public void testBuilder(Consumer action) { + ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); + action.accept(builder); + ImmutableContextSet set = builder.build(); + + ImmutableSet expected = ImmutableSet.of( + new ContextImpl("test", "a"), + new ContextImpl("test", "b"), + new ContextImpl("test", "c") + ); + + assertEquals(expected, set.toSet()); + assertEquals(3, set.size()); + + assertTrue(set.contains("test", "a")); + assertTrue(set.contains("test", "b")); + assertTrue(set.contains("test", "c")); + } + + @Test + public void testContains() { + ImmutableContextSet set = new ImmutableContextSetImpl.BuilderImpl() + .add("test", "a") + .add("test", "a") + .add("test", "b") + .add("test", "c") + .build(); + + assertTrue(set.contains("test", "a")); + assertFalse(set.contains("test", "z")); + assertFalse(set.contains("aaa", "a")); + + assertTrue(set.containsKey("test")); + assertFalse(set.containsKey("aaa")); + } + + private static Stream> testContainsAllTrue() { + return Stream.of( + builder -> builder.add("aaa", "a").add("bbb", "a"), + builder -> builder.add("aaa", "b").add("bbb", "a"), + builder -> builder.add("aaa", "c").add("bbb", "a"), + builder -> builder.add("aaa", "c").add("bbb", "b") + ); + } + + @ParameterizedTest + @MethodSource + public void testContainsAllTrue(Consumer setup) { + ImmutableContextSetImpl set = (ImmutableContextSetImpl) new ImmutableContextSetImpl.BuilderImpl() + .add("aaa", "a") + .add("aaa", "b") + .add("aaa", "c") + .add("bbb", "a") + .add("bbb", "b") + .build(); + + ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); + setup.accept(builder); + + assertTrue(set.otherContainsAll( + builder.build(), + ContextSatisfyMode.AT_LEAST_ONE_VALUE_PER_KEY) + ); + } + + private static Stream> testContainsAllFalse() { + return Stream.of( + builder -> builder.add("aaa", "a").add("bbb", "z"), + builder -> builder.add("aaa", "b").add("bbb", "z"), + builder -> builder.add("aaa", "b"), + builder -> builder.add("aaa", "c"), + builder -> {} + ); + } + + @ParameterizedTest + @MethodSource + public void testContainsAllFalse(Consumer setup) { + ImmutableContextSetImpl set = (ImmutableContextSetImpl) new ImmutableContextSetImpl.BuilderImpl() + .add("aaa", "a") + .add("aaa", "b") + .add("aaa", "c") + .add("bbb", "a") + .add("bbb", "b") + .build(); + + ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); + setup.accept(builder); + + assertFalse(set.otherContainsAll( + builder.build(), + ContextSatisfyMode.AT_LEAST_ONE_VALUE_PER_KEY) + ); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyChecksumTest.java b/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyChecksumTest.java index a73380455..8e05d23ee 100644 --- a/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyChecksumTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyChecksumTest.java @@ -25,54 +25,21 @@ package me.lucko.luckperms.common.dependencies; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import java.util.Base64; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -public class DependencyChecksumTest { - - @Test - @Tag("dependency_checksum") - public void check() { - Dependency[] dependencies = Dependency.values(); - DependencyRepository[] repos = DependencyRepository.values(); - ExecutorService pool = Executors.newCachedThreadPool(); +import static org.junit.jupiter.api.Assertions.assertTrue; - AtomicBoolean failed = new AtomicBoolean(false); - - for (Dependency dependency : dependencies) { - for (DependencyRepository repo : repos) { - pool.submit(() -> { - try { - byte[] hash = Dependency.createDigest().digest(repo.downloadRaw(dependency)); - if (!dependency.checksumMatches(hash)) { - System.out.println("NO MATCH - " + repo.name() + " - " + dependency.name() + ": " + Base64.getEncoder().encodeToString(hash)); - failed.set(true); - } else { - System.out.println("OK - " + repo.name() + " - " + dependency.name()); - } - } catch (Exception e) { - e.printStackTrace(); - } - }); - } - } - - pool.shutdown(); - try { - pool.awaitTermination(1, TimeUnit.HOURS); - } catch (InterruptedException e) { - e.printStackTrace(); - } +public class DependencyChecksumTest { - if (failed.get()) { - Assertions.fail("Some dependency checksums did not match"); + @ParameterizedTest + @EnumSource + public void checksumMatches(Dependency dependency) throws DependencyDownloadException { + for (DependencyRepository repo : DependencyRepository.REMOTE_MAVEN_REPOSITORIES) { + byte[] hash = Dependency.createDigest().digest(repo.downloadRaw(dependency)); + assertTrue(dependency.checksumMatches(hash), "Dependency " + dependency.name() + " has hash " + Base64.getEncoder().encodeToString(hash)); } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/events/UpdateEventHandler.java b/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyExportTest.java similarity index 65% rename from sponge/src/main/java/me/lucko/luckperms/sponge/service/events/UpdateEventHandler.java rename to common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyExportTest.java index a2fc9e4b4..121781c0a 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/events/UpdateEventHandler.java +++ b/common/src/test/java/me/lucko/luckperms/common/dependencies/DependencyExportTest.java @@ -23,20 +23,26 @@ * SOFTWARE. */ -package me.lucko.luckperms.sponge.service.events; +package me.lucko.luckperms.common.dependencies; -import me.lucko.luckperms.sponge.LPSpongePlugin; -import me.lucko.luckperms.sponge.service.model.LPSubjectData; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; -import org.spongepowered.api.event.permission.SubjectDataUpdateEvent; +import java.util.Base64; -public final class UpdateEventHandler { - private UpdateEventHandler() {} +public class DependencyExportTest { - public static void fireUpdateEvent(LPSpongePlugin plugin, LPSubjectData subjectData) { - plugin.getBootstrap().getScheduler().executeAsync(() -> { - SubjectDataUpdateEvent event = new LPSubjectDataUpdateEvent(plugin, subjectData); - plugin.getBootstrap().getGame().getEventManager().post(event); - }); + @Test + @Disabled + public void print() { + for (Dependency dependency : Dependency.values()) { + System.out.printf( + "[name: \"%s\", url: \"%s\", sha256: \"%s\"],\n", + dependency.getFileName(null) + "injar", + "https://repo1.maven.org/maven2/" + dependency.getMavenRepoPath(), + Base64.getEncoder().encodeToString(dependency.getChecksum()) + ); + } } + } diff --git a/common/src/test/java/me/lucko/luckperms/common/event/EventGeneratorTest.java b/common/src/test/java/me/lucko/luckperms/common/event/EventGeneratorTest.java new file mode 100644 index 000000000..1e409947c --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/event/EventGeneratorTest.java @@ -0,0 +1,94 @@ +/* + * 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.event; + +import me.lucko.luckperms.common.event.gen.GeneratedEventClass; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.event.LuckPermsEvent; +import net.luckperms.api.event.player.PlayerDataSaveEvent; +import net.luckperms.api.event.sync.PreSyncEvent; +import net.luckperms.api.model.PlayerSaveResult; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class EventGeneratorTest { + + @Mock public LuckPerms luckPermsApi; + + @Test + public void testGenerateAll() { + GeneratedEventClass.preGenerate(); + } + + @Test + public void testSimple() throws Throwable { + UUID randomUniqueId = UUID.randomUUID(); + String randomUsername = "random"; + PlayerSaveResult mockResult = mock(PlayerSaveResult.class); + + GeneratedEventClass eventClass = GeneratedEventClass.generate(PlayerDataSaveEvent.class); + LuckPermsEvent rawEvent = eventClass.newInstance(this.luckPermsApi, randomUniqueId, randomUsername, mockResult); + + assertTrue(rawEvent instanceof PlayerDataSaveEvent); + PlayerDataSaveEvent event = (PlayerDataSaveEvent) rawEvent; + + assertEquals(PlayerDataSaveEvent.class, event.getEventType()); + assertSame(randomUniqueId, event.getUniqueId()); + assertSame(randomUsername, event.getUsername()); + assertSame(mockResult, event.getResult()); + assertSame(this.luckPermsApi, event.getLuckPerms()); + } + + @Test + public void testDefaultMethods() throws Throwable { + AtomicBoolean state = new AtomicBoolean(false); + GeneratedEventClass eventClass = GeneratedEventClass.generate(PreSyncEvent.class); + PreSyncEvent event = (PreSyncEvent) eventClass.newInstance(this.luckPermsApi, state); + + assertFalse(event.isCancelled()); + assertTrue(event.isNotCancelled()); + assertEquals("PreSyncEvent{cancellationState=false}", event.toString()); + + assertFalse(event.setCancelled(true)); + + assertTrue(event.isCancelled()); + assertFalse(event.isNotCancelled()); + assertEquals("PreSyncEvent{cancellationState=true}", event.toString()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/filter/ComparisonTest.java b/common/src/test/java/me/lucko/luckperms/common/filter/ComparisonTest.java new file mode 100644 index 000000000..dc75dacb2 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/filter/ComparisonTest.java @@ -0,0 +1,81 @@ +/* + * 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.filter; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ComparisonTest { + + @ParameterizedTest(name = "[{index}] {0} {1}") + @CsvSource({ + "foo, foo, true", + "foo, Foo, true", + "Foo, foo, true", + "foo, bar, false", + "foo, '', false", + "'', foo, false", + }) + public void testEquals(String expression, String test, boolean expected) { + assertEquals(expected, ConstraintFactory.STRINGS.build(Comparison.EQUAL, expression).evaluate(test)); + assertEquals(expected, ConstraintFactory.STRINGS.build(Comparison.EQUAL, test).evaluate(expression)); + assertEquals(!expected, ConstraintFactory.STRINGS.build(Comparison.NOT_EQUAL, expression).evaluate(test)); + assertEquals(!expected, ConstraintFactory.STRINGS.build(Comparison.NOT_EQUAL, test).evaluate(expression)); + } + + @ParameterizedTest(name = "[{index}] {0} {1}") + @CsvSource({ + "foo, foo, true", + "foo, Foo, true", + "Foo, foo, true", + "foo, bar, false", + "foo, '', false", + "'', foo, false", + + "foo%, foobar, true", + "foo%, Foobar, true", + "foo%, foo, true", + "%bar%, bar, true", + "%bar%, foobar, true", + "%bar%, barbaz, true", + "%bar%, foobarbaz, true", + + "_ar, bar, true", + "_ar, far, true", + "_ar, BAR, true", + "_ar, FAR, true", + "_ar, ar, false", + "_ar, bbar, false", + }) + public void testSimilar(String expression, String test, boolean expected) { + assertEquals(expected, ConstraintFactory.STRINGS.build(Comparison.SIMILAR, expression).evaluate(test)); + assertEquals(!expected, ConstraintFactory.STRINGS.build(Comparison.NOT_SIMILAR, expression).evaluate(test)); + } + +} + diff --git a/common/src/test/java/me/lucko/luckperms/common/filter/FilterMongoTest.java b/common/src/test/java/me/lucko/luckperms/common/filter/FilterMongoTest.java new file mode 100644 index 000000000..913bb0f2f --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/filter/FilterMongoTest.java @@ -0,0 +1,133 @@ +/* + * 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.filter; + +import me.lucko.luckperms.common.filter.mongo.FilterMongoBuilder; +import org.bson.BsonDocument; +import org.bson.UuidRepresentation; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.conversions.Bson; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Locale; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FilterMongoTest { + + private static Stream testQueries() { + return Stream.of( + Arguments.of( + FilterList.empty(), + // {} + "{}" + ), + Arguments.of( + FilterList.and( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS) + ), + // {"$and": [{"foo": "hello"}]} + "{\"$and\": [{\"foo\": \"hello\"}]}" + ), + Arguments.of( + FilterList.and( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isEqualTo("world", ConstraintFactory.STRINGS) + ), + // {"$and": [{"foo": "hello"}, {"bar": "world"}]} + "{\"$and\": [{\"foo\": \"hello\"}, {\"bar\": \"world\"}]}" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS) + ), + // {"$or": [{"foo": "hello"}]} + "{\"$or\": [{\"foo\": \"hello\"}]}" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isEqualTo("world", ConstraintFactory.STRINGS) + ), + // {"$or": [{"foo": "hello"}, {"bar": "world"}]} + "{\"$or\": [{\"foo\": \"hello\"}, {\"bar\": \"world\"}]}" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isNotEqualTo("world", ConstraintFactory.STRINGS), + TestField.BAZ.isSimilarTo("abc%xyz", ConstraintFactory.STRINGS), + TestField.BAZ.isNotSimilarTo("a_c", ConstraintFactory.STRINGS) + ), + // {"$or": [ + // {"foo": "hello"}, + // {"bar": {"$ne": "world"}}, + // {"baz": {"$regularExpression": {"pattern": "abc.*xyz", "options": "i"}}}, + // {"baz": {"$not": {"$regularExpression": {"pattern": "a.c", "options": "i"}}}} + // ]} + "{\"$or\": [{\"foo\": \"hello\"}, {\"bar\": {\"$ne\": \"world\"}}, {\"baz\": {\"$regularExpression\": {\"pattern\": \"abc.*xyz\", \"options\": \"i\"}}}, {\"baz\": {\"$not\": {\"$regularExpression\": {\"pattern\": \"a.c\", \"options\": \"i\"}}}}]}" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testQueries(FilterList filters, String expectedQuery) { + Bson bson = new TestFilterMongoBuilder().make(filters); + + CodecRegistry codec = CodecRegistries.withUuidRepresentation(Bson.DEFAULT_CODEC_REGISTRY, UuidRepresentation.STANDARD); + String json = bson.toBsonDocument(BsonDocument.class, codec).toJson(); + + assertEquals(expectedQuery, json); + } + + private enum TestField implements FilterField { + FOO, BAR, BAZ; + + @Override + public String getValue(Object object) { + return "null"; + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + } + + private static final class TestFilterMongoBuilder extends FilterMongoBuilder { + + @Override + public String mapFieldName(FilterField field) { + return field.toString(); + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/filter/FilterSqlTest.java b/common/src/test/java/me/lucko/luckperms/common/filter/FilterSqlTest.java new file mode 100644 index 000000000..826c8908b --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/filter/FilterSqlTest.java @@ -0,0 +1,125 @@ +/* + * 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.filter; + +import me.lucko.luckperms.common.filter.sql.FilterSqlBuilder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Locale; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FilterSqlTest { + + private static Stream testQueries() { + return Stream.of( + Arguments.of( + FilterList.empty(), + "", + "" + ), + Arguments.of( + FilterList.and( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS) + ), + " WHERE foo = hello", + " WHERE foo = ?" + ), + Arguments.of( + FilterList.and( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isEqualTo("world", ConstraintFactory.STRINGS) + ), + " WHERE foo = hello AND bar = world", + " WHERE foo = ? AND bar = ?" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS) + ), + " WHERE foo = hello", + " WHERE foo = ?" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isEqualTo("world", ConstraintFactory.STRINGS) + ), + " WHERE foo = hello OR bar = world", + " WHERE foo = ? OR bar = ?" + ), + Arguments.of( + FilterList.or( + TestField.FOO.isEqualTo("hello", ConstraintFactory.STRINGS), + TestField.BAR.isNotEqualTo("world", ConstraintFactory.STRINGS), + TestField.BAZ.isSimilarTo("abc%xyz", ConstraintFactory.STRINGS), + TestField.BAZ.isNotSimilarTo("a_c", ConstraintFactory.STRINGS) + ), + " WHERE foo = hello OR bar != world OR baz LIKE abc%xyz OR baz NOT LIKE a_c", + " WHERE foo = ? OR bar != ? OR baz LIKE ? OR baz NOT LIKE ?" + ) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testQueries(FilterList filters, String expectedSql, String expectedSqlParams) { + FilterSqlBuilder sqlBuilder = new TestFilterSqlBuilder(); + sqlBuilder.visit(filters); + + System.out.println(sqlBuilder.builder().toReadableString()); + System.out.println(sqlBuilder.builder().toQueryString()); + + assertEquals(expectedSql, sqlBuilder.builder().toReadableString()); + assertEquals(expectedSqlParams, sqlBuilder.builder().toQueryString()); + } + + private enum TestField implements FilterField { + FOO, BAR, BAZ; + + @Override + public String getValue(Object object) { + return "null"; + } + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } + } + + private static final class TestFilterSqlBuilder extends FilterSqlBuilder { + + @Override + public void visitFieldName(FilterField field) { + this.builder.append(field.toString()); + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/filter/PageParametersTest.java b/common/src/test/java/me/lucko/luckperms/common/filter/PageParametersTest.java new file mode 100644 index 000000000..d7fe4d6fb --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/filter/PageParametersTest.java @@ -0,0 +1,50 @@ +/* + * 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.filter; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PageParametersTest { + + @ParameterizedTest(name = "[{index}] {0} {1}") + @CsvSource({ + "5, 150, 30", + "151, 150, 1", + "150, 150, 1", + "149, 150, 2", + "1, 1, 1", + "1, 0, 0", + "10, 0, 0", + }) + public void testMaxPage(int pageSize, int total, int expectedMaxPage) { + int maxPage = new PageParameters(pageSize, 1).getMaxPage(total); + assertEquals(expectedMaxPage, maxPage); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/graph/TraversalAlgorithmTest.java b/common/src/test/java/me/lucko/luckperms/common/graph/TraversalAlgorithmTest.java new file mode 100644 index 000000000..fe8b5c146 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/graph/TraversalAlgorithmTest.java @@ -0,0 +1,87 @@ +/* + * 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.graph; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TraversalAlgorithmTest { + + private static final Node ROOT = new Node("root", + new Node("a", + new Node("a1"), + new Node("a2") + ), + new Node("b", + new Node("b1"), + new Node("b2") + ) + ); + + private static final Graph GRAPH = Node::children; + + @ParameterizedTest + @CsvSource({ + "BREADTH_FIRST, 'root, a, b, a1, a2, b1, b2'", + "DEPTH_FIRST_PRE_ORDER, 'root, a, a1, a2, b, b1, b2'", + "DEPTH_FIRST_POST_ORDER, 'a1, a2, a, b1, b2, b, root'" + }) + public void testTraversal(TraversalAlgorithm alg, String expected) { + Iterable result = GRAPH.traverse(alg, ROOT); + String resultString = StreamSupport.stream(result.spliterator(), false) + .map(n -> n.name) + .collect(Collectors.joining(", ")); + + assertEquals(expected, resultString); + } + + static class Node { + private final String name; + private final List children; + + public Node(String name, Node... children) { + this.name = name; + this.children = Arrays.asList(children); + } + + public List children() { + return this.children; + } + + @Override + public String toString() { + return this.name + "(" + this.children + ")"; + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/inheritance/InheritanceComparatorTest.java b/common/src/test/java/me/lucko/luckperms/common/inheritance/InheritanceComparatorTest.java new file mode 100644 index 000000000..54ecaaaf8 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/inheritance/InheritanceComparatorTest.java @@ -0,0 +1,113 @@ +/* + * 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.inheritance; + +import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.HolderType; +import me.lucko.luckperms.common.model.PermissionHolder; +import me.lucko.luckperms.common.model.PrimaryGroupHolder; +import me.lucko.luckperms.common.model.User; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Comparator; +import java.util.List; +import java.util.OptionalInt; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +public class InheritanceComparatorTest { + + @Test + public void testUserAlwaysFirst() { + User user = makeUser("root", "admin"); + List holders = List.of( + makeGroup("admin", 0), + makeGroup("moderator", 0), + user + ); + + Comparator comparator = InheritanceComparator.getFor(user); + + List results = holders.stream().sorted(comparator).map(PermissionHolder::getPlainDisplayName).collect(Collectors.toList()); + assertEquals(List.of("root", "admin", "moderator"), results); + } + + @Test + public void testPrimaryGroupOrdering() { + User user = makeUser("root", "admin"); + List holders = List.of( + makeGroup("vip", 0), + makeGroup("admin", 0), + makeGroup("moderator", 0) + ); + + Comparator comparator = InheritanceComparator.getFor(user); + + List results = holders.stream().sorted(comparator).map(PermissionHolder::getPlainDisplayName).collect(Collectors.toList()); + assertEquals(List.of("admin", "vip", "moderator"), results); + } + + @Test + public void testWeightOrdering() { + User user = makeUser("root", "vip"); + List holders = List.of( + makeGroup("vip", 3), + makeGroup("admin", 10), + makeGroup("moderator", 5) + ); + + Comparator comparator = InheritanceComparator.getFor(user); + + List results = holders.stream().sorted(comparator).map(PermissionHolder::getPlainDisplayName).collect(Collectors.toList()); + assertEquals(List.of("admin", "moderator", "vip"), results); + } + + private static Group makeGroup(String name, int weight) { + Group group = Mockito.mock(Group.class); + when(group.getType()).thenReturn(HolderType.GROUP); + when(group.getName()).thenReturn(name); + when(group.getPlainDisplayName()).thenReturn(name); + when(group.getWeight()).thenReturn(OptionalInt.of(weight)); + return group; + } + + private static User makeUser(String name, String primaryGroup) { + User user = Mockito.mock(User.class); + when(user.getType()).thenReturn(HolderType.USER); + when(user.getPlainDisplayName()).thenReturn(name); + + PrimaryGroupHolder primaryGroupHolder = new PrimaryGroupHolder.Stored(user); + if (primaryGroup != null) { + primaryGroupHolder.setStoredValue(primaryGroup); + } + + when(user.getPrimaryGroup()).thenReturn(primaryGroupHolder); + return user; + } +} diff --git a/common/src/test/java/me/lucko/luckperms/common/locale/MessageTest.java b/common/src/test/java/me/lucko/luckperms/common/locale/MessageTest.java new file mode 100644 index 000000000..8de84b4f9 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/locale/MessageTest.java @@ -0,0 +1,298 @@ +/* + * 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.locale; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.command.spec.Argument; +import me.lucko.luckperms.common.command.spec.CommandSpec; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.extension.SimpleExtensionManager; +import me.lucko.luckperms.common.model.HolderType; +import me.lucko.luckperms.common.model.InheritanceOrigin; +import me.lucko.luckperms.common.model.PermissionHolder; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; +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.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.storage.Storage; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentIteratorFlag; +import net.kyori.adventure.text.ComponentIteratorType; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.translation.TranslationRegistry; +import net.kyori.adventure.util.UTF8ResourceBundleControl; +import net.luckperms.api.actionlog.Action; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; +import net.luckperms.api.node.types.ChatMetaNode; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.node.types.MetaNode; +import net.luckperms.api.platform.Platform; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Answers; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.text.MessageFormat; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class MessageTest { + + private static final Set> MESSAGE_CLASSES = ImmutableSet.of( + Message.Args0.class, + Message.Args1.class, + Message.Args2.class, + Message.Args3.class, + Message.Args4.class, + Message.Args5.class, + Message.Args6.class + ); + + private static final Set IGNORED_MISSING_TRANSLATION_KEYS = ImmutableSet.of( + "luckperms.command.misc.invalid-input-empty-stub" + ); + + private static TranslationRegistry registry; + private static Set translationKeys; + + @BeforeAll + public static void setupRenderer() { + registry = TranslationRegistry.create(Key.key("luckperms", "test")); + + ResourceBundle bundle = ResourceBundle.getBundle("luckperms", Locale.ENGLISH, UTF8ResourceBundleControl.get()); + translationKeys = ImmutableSet.copyOf(bundle.keySet()); + registry.registerAll(Locale.ENGLISH, bundle, false); + } + + private static Stream getMessageFields() { + return Arrays.stream(Message.class.getDeclaredFields()) + .filter(f -> Modifier.isStatic(f.getModifiers())) + .filter(f -> MESSAGE_CLASSES.contains(f.getType())); + } + + @ParameterizedTest + @MethodSource("getMessageFields") + public void testMessage(Field field) { + Component baseComponent = buildMessage(field); + for (Component part : getNestedComponents(baseComponent)) { + if (part instanceof TranslatableComponent) { + TranslatableComponent component = (TranslatableComponent) part; + assertTranslatableComponentValid(component); + } + } + } + + @ParameterizedTest + @EnumSource + public void testCommandUsageMessages(CommandSpec commandSpec) { + assertTranslatableComponentValid(commandSpec.description()); + + List args = commandSpec.args(); + if (args != null) { + for (Argument arg : args) { + assertTranslatableComponentValid(arg.getDescription()); + } + } + } + + private static void assertTranslatableComponentValid(TranslatableComponent component) { + String key = component.key(); + + if (IGNORED_MISSING_TRANSLATION_KEYS.contains(key)) { + return; + } + + assertTrue(translationKeys.contains(key), "unknown translation key: " + key); + + List args = component.args(); + MessageFormat fmt = registry.translate(key, Locale.ENGLISH); + assertNotNull(fmt); + assertEquals(fmt.getFormats().length, args.size(), "number of formats in translation for " + key + " does not match number of arguments"); + } + + private static Iterable getNestedComponents(Component component) { + return component.iterable( + ComponentIteratorType.BREADTH_FIRST, + ImmutableSet.of(ComponentIteratorFlag.INCLUDE_TRANSLATABLE_COMPONENT_ARGUMENTS, ComponentIteratorFlag.INCLUDE_HOVER_SHOW_TEXT_COMPONENT) + ); + } + + private static Component buildMessage(Field field) { + Class type = field.getType(); + + List buildMethods = Arrays.stream(type.getDeclaredMethods()) + .filter(method -> method.getName().equals("build")) + .collect(Collectors.toList()); + + if (buildMethods.size() != 1) { + throw new IllegalStateException("Expected exactly one build() method - " + buildMethods); + } + + Method buildMethod = buildMethods.get(0); + Object[] parameters = new Object[buildMethod.getParameterCount()]; + + if (buildMethod.getParameterCount() != 0) { + Type genericType = field.getGenericType(); + Type[] typeArguments = ((ParameterizedType) genericType).getActualTypeArguments(); + for (int i = 0; i < typeArguments.length; i++) { + Type typeArgument = typeArguments[i]; + parameters[i] = mockArgument(typeArgument); + } + } + + try { + Object builder = field.get(null); + return (Component) buildMethod.invoke(builder, parameters); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private static Object mockArgument(Type type) { + if (type instanceof ParameterizedType) { + return mockArgument(((ParameterizedType) type).getRawType()); + } + + Class clazz = (Class) type; + + if (clazz == String.class) { + return "stub"; + } else if (clazz == Integer.class) { + return 0; + } else if (clazz == Boolean.class) { + return false; + } else if (clazz == Double.class) { + return 0d; + } else if (clazz == LoggedAction.class) { + return LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("stub") + .targetType(Action.Target.Type.GROUP) + .targetName("stub") + .description("stub") + .build(); + } else if (clazz == Node.class) { + return Permission.builder().permission("stub").expiry(1, TimeUnit.MINUTES).build(); + } else if (clazz == InheritanceNode.class) { + return Inheritance.builder().group("stub").expiry(1, TimeUnit.MINUTES).build(); + } else if (clazz == MetaNode.class) { + return Meta.builder("stub", "stub") + .withMetadata(InheritanceOriginMetadata.KEY, new InheritanceOrigin( + new PermissionHolderIdentifier(HolderType.GROUP, "stub"), + DataType.NORMAL + )) + .build(); + } else if (clazz == ChatMetaNode.class) { + return Prefix.builder("stub", 1) + .withMetadata(InheritanceOriginMetadata.KEY, new InheritanceOrigin( + new PermissionHolderIdentifier(HolderType.GROUP, "stub"), + DataType.NORMAL + )) + .build(); + } else if (clazz == ContextSet.class) { + return ImmutableContextSetImpl.of("stub", "stub"); + } else if (clazz == Component.class) { + return Component.text("stub"); + } else if (clazz == List.class) { + return ImmutableList.of(); + } else if (clazz == Collection.class) { + return ImmutableList.of(); + } + + Object mock; + if (clazz == LuckPermsPlugin.class) { + mock = mock(clazz, Answers.RETURNS_DEEP_STUBS); + } else { + mock = mock(clazz, Answers.RETURNS_SMART_NULLS); + } + + if (mock instanceof LuckPermsBootstrap) { + LuckPermsBootstrap bootstrap = (LuckPermsBootstrap) mock; + lenient().when(bootstrap.getType()).thenReturn(Platform.Type.BUKKIT); + lenient().when(bootstrap.getStartupTime()).thenReturn(Instant.now()); + } else if (mock instanceof LuckPermsPlugin) { + LuckPermsPlugin plugin = (LuckPermsPlugin) mock; + + LuckPermsBootstrap bootstrap = (LuckPermsBootstrap) mockArgument(LuckPermsBootstrap.class); + lenient().when(plugin.getBootstrap()).thenReturn(bootstrap); + + Storage storage = (Storage) mockArgument(Storage.class); + lenient().when(plugin.getStorage()).thenReturn(storage); + + SimpleExtensionManager extManager = (SimpleExtensionManager) mockArgument(SimpleExtensionManager.class); + lenient().when(plugin.getExtensionManager()).thenReturn(extManager); + + lenient().when(plugin.getMessagingService()).thenReturn(Optional.empty()); + } else if (mock instanceof PermissionHolder) { + PermissionHolder holder = (PermissionHolder) mock; + + LuckPermsPlugin plugin = (LuckPermsPlugin) mockArgument(LuckPermsPlugin.class); + lenient().when(holder.getPlugin()).thenReturn(plugin); + + lenient().when(holder.getFormattedDisplayName()).thenReturn(Component.text("stub")); + } else if (mock instanceof SimpleExtensionManager) { + SimpleExtensionManager manager = (SimpleExtensionManager) mock; + lenient().when(manager.getLoadedExtensions()).thenReturn(ImmutableList.of()); + } + + return mock; + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/locale/TranslationTest.java b/common/src/test/java/me/lucko/luckperms/common/locale/TranslationTest.java new file mode 100644 index 000000000..8d95a9e28 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/locale/TranslationTest.java @@ -0,0 +1,50 @@ +/* + * 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.locale; + +import net.kyori.adventure.util.UTF8ResourceBundleControl; +import org.junit.jupiter.api.Test; + +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TranslationTest { + + @Test + public void testBundleParse() { + ResourceBundle bundle = ResourceBundle.getBundle("luckperms", Locale.ENGLISH, UTF8ResourceBundleControl.get()); + Set keys = bundle.keySet(); + assertTrue(keys.size() > 100); + + for (String key : keys) { + assertTrue(key.startsWith("luckperms."), "key " + key + " should start with 'luckperms.'"); + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/messaging/MessageImplTest.java b/common/src/test/java/me/lucko/luckperms/common/messaging/MessageImplTest.java new file mode 100644 index 000000000..9da7a5a4f --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/messaging/MessageImplTest.java @@ -0,0 +1,116 @@ +/* + * 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.messaging; + +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.actionlog.ActionJsonSerializer; +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.messaging.message.ActionLogMessageImpl; +import me.lucko.luckperms.common.messaging.message.CustomMessageImpl; +import me.lucko.luckperms.common.messaging.message.UpdateMessageImpl; +import me.lucko.luckperms.common.messaging.message.UserUpdateMessageImpl; +import me.lucko.luckperms.common.util.gson.JObject; +import net.luckperms.api.actionlog.Action; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MessageImplTest { + + @Test + public void testUpdateMessage() { + UUID uuid = UUID.fromString("22f9e168-8815-44f1-83c8-b642ebfbcef2"); + + // encode + UpdateMessageImpl msg = new UpdateMessageImpl(uuid); + assertEquals("{\"id\":\"22f9e168-8815-44f1-83c8-b642ebfbcef2\",\"type\":\"update\"}", msg.asEncodedString()); + + // decode + msg = UpdateMessageImpl.decode(new JsonObject(), uuid); + assertEquals(uuid, msg.getId()); + } + + @Test + public void testUserUpdateMessage() { + UUID uuid = UUID.fromString("22f9e168-8815-44f1-83c8-b642ebfbcef2"); + UUID userUuid = UUID.fromString("4c087cd9-f444-4c52-8438-e03e57ba2e8e"); + + // encode + UserUpdateMessageImpl msg = new UserUpdateMessageImpl(uuid, userUuid); + // {"id":"22f9e168-8815-44f1-83c8-b642ebfbcef2","type":"userupdate","content":{"userUuid":"4c087cd9-f444-4c52-8438-e03e57ba2e8e"}} + assertEquals("{\"id\":\"22f9e168-8815-44f1-83c8-b642ebfbcef2\",\"type\":\"userupdate\",\"content\":{\"userUuid\":\"4c087cd9-f444-4c52-8438-e03e57ba2e8e\"}}", msg.asEncodedString()); + + // decode + msg = UserUpdateMessageImpl.decode(new JObject().add("userUuid", userUuid.toString()).toJson(), uuid); + assertEquals(uuid, msg.getId()); + assertEquals(userUuid, msg.getUserUniqueId()); + } + + @Test + public void testActionLogMessage() { + UUID uuid = UUID.fromString("22f9e168-8815-44f1-83c8-b642ebfbcef2"); + LoggedAction action = LoggedAction.build() + .source(UUID.fromString("d3500320-564c-436e-87a0-026d7f2c92f6")) + .sourceName("Test") + .targetType(Action.Target.Type.GROUP) + .targetName("test") + .description("test") + .timestamp(Instant.ofEpochSecond(1)) + .build(); + + // encode + ActionLogMessageImpl msg = new ActionLogMessageImpl(uuid, action); + // {"id":"22f9e168-8815-44f1-83c8-b642ebfbcef2","type":"log","content":{"timestamp":1,"source":{"uniqueId":"d3500320-564c-436e-87a0-026d7f2c92f6","name":"Test"},"target":{"type":"GROUP","name":"test"},"description":"test"}} + assertEquals("{\"id\":\"22f9e168-8815-44f1-83c8-b642ebfbcef2\",\"type\":\"log\",\"content\":{\"timestamp\":1,\"source\":{\"uniqueId\":\"d3500320-564c-436e-87a0-026d7f2c92f6\",\"name\":\"Test\"},\"target\":{\"type\":\"GROUP\",\"name\":\"test\"},\"description\":\"test\"}}", msg.asEncodedString()); + + // decode + msg = ActionLogMessageImpl.decode(ActionJsonSerializer.serialize(action), uuid); + assertEquals(uuid, msg.getId()); + assertEquals(action, msg.getAction()); + } + + @Test + public void testCustomMessage() { + UUID uuid = UUID.fromString("22f9e168-8815-44f1-83c8-b642ebfbcef2"); + String channelId = "test"; + String payload = "test"; + + // encode + CustomMessageImpl msg = new CustomMessageImpl(uuid, channelId, payload); + // {"id":"22f9e168-8815-44f1-83c8-b642ebfbcef2","type":"custom","content":{"channelId":"test","payload":"test"}} + assertEquals("{\"id\":\"22f9e168-8815-44f1-83c8-b642ebfbcef2\",\"type\":\"custom\",\"content\":{\"channelId\":\"test\",\"payload\":\"test\"}}", msg.asEncodedString()); + + // decode + msg = CustomMessageImpl.decode(new JObject().add("channelId", channelId).add("payload", payload).toJson(), uuid); + assertEquals(uuid, msg.getId()); + assertEquals(channelId, msg.getChannelId()); + assertEquals(payload, msg.getPayload()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/GroupManagerTest.java b/common/src/test/java/me/lucko/luckperms/common/model/GroupManagerTest.java new file mode 100644 index 000000000..e235c7cb3 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/GroupManagerTest.java @@ -0,0 +1,91 @@ +/* + * 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.model; + +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class GroupManagerTest { + + @Mock private LuckPermsPlugin plugin; + + @Test + public void testSanitizeIdentifier() { + StandardGroupManager manager = new StandardGroupManager(this.plugin) { + @Override + public Group apply(String name) { + Group group = mock(Group.class); + when(group.getName()).thenReturn(name); + return group; + } + }; + + Group group = manager.getOrMake("DEFAULT"); + assertEquals("default", group.getName()); + assertEquals(ImmutableSet.of("default"), manager.getAll().keySet()); + } + + @Test + public void testGetByDisplayName() { + StandardGroupManager manager = new StandardGroupManager(this.plugin) { + @Override + public Group apply(String name) { + return mock(Group.class); + } + }; + + Group defaultGroup = manager.getOrMake("default"); + when(defaultGroup.getDisplayName()).thenReturn(Optional.of("member")); + + assertSame(defaultGroup, manager.getByDisplayName("default")); + assertSame(defaultGroup, manager.getByDisplayName("Default")); + assertSame(defaultGroup, manager.getByDisplayName("member")); + assertSame(defaultGroup, manager.getByDisplayName("Member")); + assertNull(manager.getByDisplayName("test")); + + Group memberGroup = manager.getOrMake("member"); + + assertSame(defaultGroup, manager.getByDisplayName("default")); + assertSame(defaultGroup, manager.getByDisplayName("Default")); + assertSame(memberGroup, manager.getByDisplayName("member")); + assertSame(memberGroup, manager.getByDisplayName("Member")); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/GroupTest.java b/common/src/test/java/me/lucko/luckperms/common/model/GroupTest.java new file mode 100644 index 000000000..602b37089 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/GroupTest.java @@ -0,0 +1,136 @@ +/* + * 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.model; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.LuckPermsConfiguration; +import me.lucko.luckperms.common.context.manager.ContextManager; +import me.lucko.luckperms.common.event.EventDispatcher; +import me.lucko.luckperms.common.node.types.DisplayName; +import me.lucko.luckperms.common.node.types.Weight; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import net.luckperms.api.context.ContextSatisfyMode; +import net.luckperms.api.model.data.DataType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class GroupTest { + + @Mock private LuckPermsPlugin plugin; + @Mock private LuckPermsConfiguration configuration; + @Mock private ContextManager contextManager; + + @BeforeEach + public void setupMocks() { + lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration); + + //noinspection unchecked,rawtypes + lenient().when(this.plugin.getContextManager()).thenReturn((ContextManager) this.contextManager); + } + + @Test + public void testGetWeightEmpty() { + when(this.configuration.get(ConfigKeys.GROUP_WEIGHTS)).thenReturn(Map.of()); + + OptionalInt res = new Group("test", this.plugin).getWeight(); + assertTrue(res.isEmpty()); + } + + @Test + public void testGetWeightFromConfiguration() { + when(this.configuration.get(ConfigKeys.GROUP_WEIGHTS)).thenReturn(Map.of("test", 10)); + + OptionalInt res = new Group("test", this.plugin).getWeight(); + assertTrue(res.isPresent()); + assertEquals(10, res.getAsInt()); + } + + @Test + public void testGetWeightFromNode() { + when(this.configuration.get(ConfigKeys.CONTEXT_SATISFY_MODE)).thenReturn(ContextSatisfyMode.AT_LEAST_ONE_VALUE_PER_KEY); + + Group group = new Group("test", this.plugin); + group.setNode(DataType.NORMAL, Weight.builder(3).build(), false); + group.setNode(DataType.NORMAL, Weight.builder(5).build(), false); + + OptionalInt res = group.getWeight(); + assertTrue(res.isPresent()); + assertEquals(5, res.getAsInt()); + + group.setNode(DataType.NORMAL, Weight.builder(100).build(), false); + + res = group.getWeight(); + assertTrue(res.isPresent()); + assertEquals(100, res.getAsInt()); + } + + @Test + public void testGetDisplayNameEmpty() { + when(this.contextManager.getStaticQueryOptions()).thenReturn(QueryOptionsImpl.DEFAULT_CONTEXTUAL); + when(this.configuration.get(ConfigKeys.GROUP_NAME_REWRITES)).thenReturn(Map.of()); + + Optional displayName = new Group("test", this.plugin).getDisplayName(); + assertTrue(displayName.isEmpty()); + } + + @Test + public void testGetDisplayNameFromConfiguration() { + when(this.contextManager.getStaticQueryOptions()).thenReturn(QueryOptionsImpl.DEFAULT_CONTEXTUAL); + when(this.configuration.get(ConfigKeys.GROUP_NAME_REWRITES)).thenReturn(Map.of("test", "Test")); + + Optional displayName = new Group("test", this.plugin).getDisplayName(); + assertTrue(displayName.isPresent()); + assertEquals("Test", displayName.get()); + } + + @Test + public void testGetDisplayNameFromNode() { + when(this.contextManager.getStaticQueryOptions()).thenReturn(QueryOptionsImpl.DEFAULT_CONTEXTUAL); + + Group group = new Group("test", this.plugin); + group.setNode(DataType.NORMAL, DisplayName.builder("TEST").build(), false); + + Optional displayName = group.getDisplayName(); + assertTrue(displayName.isPresent()); + assertEquals("TEST", displayName.get()); + } +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/InheritanceTest.java b/common/src/test/java/me/lucko/luckperms/common/model/InheritanceTest.java new file mode 100644 index 000000000..0db100a0e --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/InheritanceTest.java @@ -0,0 +1,136 @@ +/* + * 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.model; + +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.graph.TraversalAlgorithm; +import me.lucko.luckperms.common.inheritance.InheritanceGraphFactory; +import me.lucko.luckperms.common.model.manager.group.GroupManager; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.common.node.types.Weight; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import net.luckperms.api.context.ContextSatisfyMode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class InheritanceTest { + + @Mock private LuckPermsPlugin plugin; + @Mock private LuckPermsConfiguration configuration; + + private StandardGroupManager groupManager; + + @BeforeEach + public void setupMocks() { + this.groupManager = new StandardGroupManager(this.plugin); + + //noinspection unchecked,rawtypes + lenient().when(this.plugin.getGroupManager()).thenReturn((GroupManager) this.groupManager); + lenient().when(this.plugin.getInheritanceGraphFactory()).thenReturn(new InheritanceGraphFactory(this.plugin)); + lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration); + lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + lenient().when(this.configuration.get(ConfigKeys.CONTEXT_SATISFY_MODE)).thenReturn(ContextSatisfyMode.AT_LEAST_ONE_VALUE_PER_KEY); + lenient().when(this.configuration.get(ConfigKeys.GROUP_WEIGHTS)).thenReturn(Collections.emptyMap()); + } + + /* + * Given the following inheritance setup: + * (value in brackets is the group weight) + * + * test user + * │ + * ├── owner (13) + * │ └── admin (12) + * │ └── mod (11) + * │ └── helper (10) + * │ └── member (0) + * └── vip+ (6) + * └── vip (5) + * └── member (0) + * + * This test checks if the resolved inheritance order is correct :) + */ + @ParameterizedTest(name = "[{index}] {0}, {1}") + @CsvSource({ + "DEPTH_FIRST_PRE_ORDER, false, 'owner -> admin -> mod -> helper -> member -> vip+ -> vip'", + "BREADTH_FIRST, false, 'owner -> vip+ -> admin -> vip -> mod -> member -> helper'", + "DEPTH_FIRST_POST_ORDER, false, 'member -> helper -> mod -> admin -> owner -> vip -> vip+'", + "DEPTH_FIRST_PRE_ORDER, true, 'owner -> admin -> mod -> helper -> vip+ -> vip -> member'", + "BREADTH_FIRST, true, 'owner -> admin -> mod -> helper -> vip+ -> vip -> member'", + "DEPTH_FIRST_POST_ORDER, true, 'owner -> admin -> mod -> helper -> vip+ -> vip -> member'" + }) + public void testInheritanceTree(TraversalAlgorithm traversalAlgorithm, boolean postTraversalSort, String expected) { + when(this.configuration.get(ConfigKeys.INHERITANCE_TRAVERSAL_ALGORITHM)).thenReturn(traversalAlgorithm); + when(this.configuration.get(ConfigKeys.POST_TRAVERSAL_INHERITANCE_SORT)).thenReturn(postTraversalSort); + + Group member = this.groupManager.getOrMake("member"); + + Group helper = createGroup("helper", 10, member); + Group mod = createGroup("mod", 11, helper); + Group admin = createGroup("admin", 12, mod); + Group owner = createGroup("owner", 13, admin); + + Group vip = createGroup("vip", 5, member); + Group vipPlus = createGroup("vip+", 6, vip); + + PermissionHolder testHolder = this.groupManager.getOrMake("test"); + testHolder.normalData().add(Inheritance.builder().group(owner.getName()).build()); + testHolder.normalData().add(Inheritance.builder().group(vipPlus.getName()).build()); + + List groups = testHolder.resolveInheritanceTree(QueryOptionsImpl.DEFAULT_CONTEXTUAL) + .stream().map(Group::getName).collect(Collectors.toList()); + + List expectedList = Arrays.stream(expected.split(" -> ")).collect(Collectors.toList()); + assertEquals(expectedList, groups); + } + + private Group createGroup(String name, int weight, Group parent) { + Group group = this.groupManager.getOrMake(name); + group.normalData().add(Inheritance.builder().group(parent.getName()).build()); + group.normalData().add(Weight.builder().weight(weight).build()); + return group; + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/NodeMapTest.java b/common/src/test/java/me/lucko/luckperms/common/model/NodeMapTest.java new file mode 100644 index 000000000..08a0df534 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/NodeMapTest.java @@ -0,0 +1,506 @@ +/* + * 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.model; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.model.nodemap.NodeMapMutable; +import me.lucko.luckperms.common.node.factory.NodeBuilders; +import me.lucko.luckperms.common.query.QueryOptionsBuilderImpl; +import me.lucko.luckperms.common.util.Difference; +import net.luckperms.api.context.ContextSatisfyMode; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.query.Flag; +import net.luckperms.api.query.QueryMode; +import net.luckperms.api.query.QueryOptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; +import java.time.Instant; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class NodeMapTest { + + private static final PermissionHolderIdentifier ORIGIN = new PermissionHolderIdentifier(HolderType.GROUP, "test"); + + @Mock private PermissionHolder mockHolder; + + @BeforeEach + public void setupMocks() { + when(this.mockHolder.getIdentifier()).thenReturn(ORIGIN); + } + + private static Node makeNode(String key) { + return NodeBuilders.determineMostApplicable(key).build(); + } + + @Test + public void testSimpleAddAndRemove() { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + assertEquals(0, map.size()); + + Node node = makeNode("test"); + + Difference r1 = map.add(node); + assertEquals(ImmutableSet.of(node), r1.getAdded()); + assertEquals(ImmutableSet.of(), r1.getRemoved()); + assertEquals(1, map.size()); + + Difference r2 = map.remove(node); + assertEquals(ImmutableSet.of(), r2.getAdded()); + assertEquals(ImmutableSet.of(node), r2.getRemoved()); + assertEquals(0, map.size()); + } + + @Test + public void testInheritanceOrigin() { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + Node node = makeNode("test"); + + map.add(node); + + List nodes = map.asList(); + assertEquals(ImmutableList.of(node), map.asList()); + + InheritanceOriginMetadata origin = nodes.get(0).metadata(InheritanceOriginMetadata.KEY); + assertEquals(ORIGIN, origin.getOrigin()); + assertEquals(DataType.NORMAL, origin.getDataType()); + } + + @ParameterizedTest + @CsvSource({ + "test, true, false", + "test, false, true", + "group.test, true, false", + "group.test, false, true" + }) + public void testRemoveMatchingButNotSameValue(String nodeKey, boolean firstValue, boolean secondValue) { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + + Node first = makeNode(nodeKey).toBuilder().value(firstValue).build(); + Node second = makeNode(nodeKey).toBuilder().value(secondValue).build(); + + map.add(first); + + Difference diff = map.add(second); + assertEquals(ImmutableSet.of(first), diff.getRemoved()); + assertEquals(ImmutableSet.of(second), diff.getAdded()); + assertEquals(ImmutableList.of(second), map.asList()); + + if (second.getType() == NodeType.INHERITANCE && second.getValue()) { + assertEquals(ImmutableList.of(second), map.inheritanceAsList()); + } else { + assertEquals(ImmutableList.of(), map.inheritanceAsList()); + } + } + + @ParameterizedTest + @CsvSource({ + "test, 1, 5", + "test, 5, 1", + "group.test, 1, 5", + "group.test, 5, 1" + }) + public void testRemoveMatchingButNotSameExpiry(String nodeKey, int firstDuration, int secondDuration) { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + + Node first = makeNode(nodeKey).toBuilder() + .expiry(firstDuration == 0 ? null : Duration.ofDays(firstDuration)) + .build(); + Node second = makeNode(nodeKey).toBuilder() + .expiry(secondDuration == 0 ? null : Duration.ofDays(secondDuration)) + .build(); + + map.add(first); + + Difference diff = map.add(second); + assertEquals(ImmutableSet.of(first), diff.getRemoved()); + assertEquals(ImmutableSet.of(second), diff.getAdded()); + assertEquals(ImmutableList.of(second), map.asList()); + + if (second.getType() == NodeType.INHERITANCE) { + assertEquals(ImmutableList.of(second), map.inheritanceAsList()); + } else { + assertEquals(ImmutableList.of(), map.inheritanceAsList()); + } + } + + @Test + public void testRemove() { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + map.add(makeNode("test1")); + map.add(makeNode("test2").toBuilder().value(false).build()); + map.add(makeNode("test3").toBuilder().expiry(1, TimeUnit.HOURS).build()); + map.add(makeNode("test4").toBuilder().withContext("hello", "world").build()); + + assertEquals(4, map.size()); + + for (Node node : List.of( + makeNode("test1").toBuilder().withContext("hello", "world").build(), + makeNode("test3"), + makeNode("test4"), + makeNode("test4").toBuilder().withContext("hello", "world").withContext("aaa", "bbb").build(), + makeNode("test5") + )) { + Difference diff = map.remove(node); + assertEquals(Set.of(), diff.getChanges()); + } + + assertEquals(4, map.size()); + + for (Node node : List.of( + makeNode("test1"), + makeNode("test2").toBuilder().value(true).build(), + makeNode("test3").toBuilder().expiry(2, TimeUnit.HOURS).build(), + makeNode("test4").toBuilder().withContext("hello", "world").build() + )) { + Difference diff = map.remove(node); + assertEquals(0, diff.getAdded().size()); + assertEquals(1, diff.getRemoved().size()); + } + + assertEquals(0, map.size()); + } + + @Test + public void testRemoveExact() { + Instant expiry = Instant.now().plusSeconds(60); + + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + map.add(makeNode("test1")); + map.add(makeNode("test2").toBuilder().value(false).build()); + map.add(makeNode("test3").toBuilder().expiry(expiry).build()); + map.add(makeNode("test4").toBuilder().withContext("hello", "world").build()); + + assertEquals(4, map.size()); + + for (Node node : List.of( + makeNode("test1").toBuilder().withContext("hello", "world").build(), + makeNode("test3"), + makeNode("test4"), + makeNode("test4").toBuilder().withContext("hello", "world").withContext("aaa", "bbb").build(), + makeNode("test5"), + makeNode("test2").toBuilder().value(true).build(), + makeNode("test3").toBuilder().expiry(2, TimeUnit.HOURS).build(), + makeNode("test4").toBuilder().withContext("hello", "world").withContext("aaa", "bbb").build() + )) { + Difference diff = map.removeExact(node); + assertEquals(Set.of(), diff.getChanges()); + } + + assertEquals(4, map.size()); + + for (Node node : List.of( + makeNode("test1"), + makeNode("test2").toBuilder().value(false).build(), + makeNode("test3").toBuilder().expiry(expiry).build(), + makeNode("test4").toBuilder().withContext("hello", "world").build() + )) { + Difference diff = map.removeExact(node); + assertEquals(0, diff.getAdded().size()); + assertEquals(1, diff.getRemoved().size()); + } + + assertEquals(0, map.size()); + } + + @Test + public void testClear() { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + map.add(makeNode("a")); + map.add(makeNode("b")); + map.add(makeNode("c")); + + assertEquals(3, map.size()); + + Difference diff = map.clear(); + assertEquals(3, diff.getRemoved().size()); + assertEquals(0, map.size()); + } + + @Test + public void testSetContent() { + Node a = makeNode("a"); + Node b = makeNode("b"); + Node c = makeNode("c"); + + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + map.add(a); + map.add(b); + + Difference diff = map.setContent(List.of(b, c)); + assertEquals(Set.of(c), diff.getAdded()); + assertEquals(Set.of(a), diff.getRemoved()); + + assertEquals(Set.of(b, c), map.asSet()); + } + + @ParameterizedTest(name = "s={0} w={1} is={2} iw={3}") + @CsvSource({ + "true, true, true, true, 8, 4", + "true, true, false, false, 8, 1", + "false, true, true, true, 6, 4", + "true, false, true, true, 6, 4", + "false, false, true, true, 5, 4", + "false, true, false, true, 4, 2", + "true, false, true, false, 4, 2", + "false, false, false, false, 2, 1" + }) + public void testFlagsFiltering(boolean includeServer, boolean includeWorld, boolean inheritanceIncludeServer, boolean inheritanceIncludeWorld, int expected, int expectedInheritance) { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL) { + @Override + protected ContextSatisfyMode defaultSatisfyMode() { + return ContextSatisfyMode.AT_LEAST_ONE_VALUE_PER_KEY; + } + }; + + map.add(makeNode("test1")); + map.add(makeNode("test2").toBuilder().withContext("server", "test").build()); + map.add(makeNode("test3").toBuilder().withContext("world", "test").build()); + map.add(makeNode("test4").toBuilder().withContext("server", "test").withContext("world", "test").build()); + map.add(makeNode("group.test1")); + map.add(makeNode("group.test2").toBuilder().withContext("server", "test").build()); + map.add(makeNode("group.test3").toBuilder().withContext("world", "test").build()); + map.add(makeNode("group.test4").toBuilder().withContext("server", "test").withContext("world", "test").build()); + + Set flags = EnumSet.noneOf(Flag.class); + if (includeServer) flags.add(Flag.INCLUDE_NODES_WITHOUT_SERVER_CONTEXT); + if (includeWorld) flags.add(Flag.INCLUDE_NODES_WITHOUT_WORLD_CONTEXT); + if (inheritanceIncludeServer) flags.add(Flag.APPLY_INHERITANCE_NODES_WITHOUT_SERVER_CONTEXT); + if (inheritanceIncludeWorld) flags.add(Flag.APPLY_INHERITANCE_NODES_WITHOUT_WORLD_CONTEXT); + + QueryOptions options = new QueryOptionsBuilderImpl(QueryMode.NON_CONTEXTUAL) + .flags(flags) + .build(); + + Set output = new HashSet<>(); + map.copyTo(output, options); + assertEquals(expected, output.size()); + + output.clear(); + map.forEach(options, output::add); + assertEquals(expected, output.size()); + + Set inheritanceOutput = new HashSet<>(); + map.copyInheritanceNodesTo(inheritanceOutput, options); + assertEquals(expectedInheritance, inheritanceOutput.size()); + } + + @Test + public void testRemoveIf() { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + Node test1 = makeNode("test1"); + Node test2 = makeNode("test2").toBuilder().value(false).build(); + Node group1 = makeNode("group.test1"); + Node group2 = makeNode("group.test2").toBuilder().value(false).build(); + + map.add(test1); + map.add(test2); + map.add(group1); + map.add(group2); + assertEquals(4, map.size()); + + Difference diff = map.removeIf(node -> !node.getValue()); + assertEquals(Set.of(test2, group2), diff.getRemoved()); + assertEquals(Set.of(), diff.getAdded()); + assertEquals(2, map.size()); + assertEquals(Set.of(test1, group1), map.asSet()); + assertEquals(List.of(group1), map.inheritanceAsList()); + } + + @Test + public void testRemoveIfWithContext() { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + Node inContext = makeNode("test1").toBuilder().withContext("server", "test").build(); + Node globalNode = makeNode("test2"); + Node otherInContext = makeNode("test3").toBuilder().withContext("server", "test").build(); + + map.add(inContext); + map.add(globalNode); + map.add(otherInContext); + assertEquals(3, map.size()); + + Difference diff = map.removeIf(inContext.getContexts(), node -> node.getKey().equals("test1")); + assertEquals(Set.of(inContext), diff.getRemoved()); + assertEquals(2, map.size()); + assertEquals(Set.of(globalNode, otherInContext), map.asSet()); + + // no-op if the context set isn't present in the map + Difference diff2 = map.removeIf(ImmutableContextSetImpl.of("world", "test"), node -> true); + assertTrue(diff2.isEmpty()); + assertEquals(2, map.size()); + } + + @ParameterizedTest + @CsvSource({ + "test1, test2, false", + "test1, test1, true" + }) + public void testRemoveThenAdd(String removeKey, String addKey, boolean sameNode) { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + Node toRemove = makeNode(removeKey); + Node toAdd = makeNode(addKey); + map.add(toRemove); + + Difference diff = map.removeThenAdd(toRemove, toAdd); + + if (sameNode) { + // removeThenAdd is a no-op if the two nodes are equal + assertTrue(diff.isEmpty()); + assertEquals(1, map.size()); + assertEquals(Set.of(toRemove), map.asSet()); + } else { + assertEquals(Set.of(toAdd), diff.getAdded()); + assertEquals(Set.of(toRemove), diff.getRemoved()); + assertEquals(Set.of(toAdd), map.asSet()); + } + } + + @Test + public void testClearContext() { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + Node global = makeNode("test1"); + Node scoped = makeNode("test2").toBuilder().withContext("server", "test").build(); + + map.add(global); + map.add(scoped); + assertEquals(2, map.size()); + + Difference diff = map.clear(scoped.getContexts()); + assertEquals(Set.of(scoped), diff.getRemoved()); + assertEquals(Set.of(global), map.asSet()); + + // clearing a context that's no longer present is a no-op + Difference diff2 = map.clear(scoped.getContexts()); + assertTrue(diff2.isEmpty()); + assertEquals(1, map.size()); + } + + @Test + public void testApplyChanges() { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + Node existing = makeNode("test1"); + Node incoming = makeNode("test2"); + map.add(existing); + + Difference changes = new Difference<>(); + changes.recordChange(Difference.ChangeType.REMOVE, existing); + changes.recordChange(Difference.ChangeType.ADD, incoming); + + Difference result = map.applyChanges(changes); + assertEquals(Set.of(incoming), result.getAdded()); + assertEquals(Set.of(existing), result.getRemoved()); + assertEquals(Set.of(incoming), map.asSet()); + } + + @Test + public void testNodesInContext() { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + Node global = makeNode("test1"); + Node scoped = makeNode("group.test2").toBuilder().withContext("server", "test").build(); + + map.add(global); + map.add(scoped); + + assertEquals(Set.of(global), Set.copyOf(map.nodesInContext(ImmutableContextSetImpl.EMPTY))); + assertEquals(Set.of(scoped), Set.copyOf(map.nodesInContext(scoped.getContexts()))); + assertEquals(Set.of(scoped), Set.copyOf(map.inheritanceNodesInContext(scoped.getContexts()))); + + // context not present in the map + assertEquals(Set.of(), map.nodesInContext(ImmutableContextSetImpl.of("world", "test"))); + } + + @ParameterizedTest + @CsvSource({ + "'', 2, 1", + "server=test, 4, 2", + "world=test, 4, 2", + "server=test|world=test, 8, 4", + "server=test|world=test|test=test, 8, 4", + }) + public void testContextFiltering(String context, int expected, int expectedInheritance) { + NodeMapMutable map = new NodeMapMutable(this.mockHolder, DataType.NORMAL) { + @Override + protected ContextSatisfyMode defaultSatisfyMode() { + return ContextSatisfyMode.AT_LEAST_ONE_VALUE_PER_KEY; + } + }; + + map.add(makeNode("test1")); + map.add(makeNode("test2").toBuilder().withContext("server", "test").build()); + map.add(makeNode("test3").toBuilder().withContext("world", "test").build()); + map.add(makeNode("test4").toBuilder().withContext("server", "test").withContext("world", "test").build()); + map.add(makeNode("group.test1")); + map.add(makeNode("group.test2").toBuilder().withContext("server", "test").build()); + map.add(makeNode("group.test3").toBuilder().withContext("world", "test").build()); + map.add(makeNode("group.test4").toBuilder().withContext("server", "test").withContext("world", "test").build()); + + ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); + if (!context.isEmpty()) { + Splitter.on('|').withKeyValueSeparator('=').split(context).forEach(builder::add); + } + + QueryOptions options = new QueryOptionsBuilderImpl(QueryMode.CONTEXTUAL) + .context(builder.build()) + .build(); + + Set output = new HashSet<>(); + map.copyTo(output, options); + assertEquals(expected, output.size()); + + output.clear(); + map.forEach(options, output::add); + assertEquals(expected, output.size()); + + Set inheritanceOutput = new HashSet<>(); + map.copyInheritanceNodesTo(inheritanceOutput, options); + assertEquals(expectedInheritance, inheritanceOutput.size()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/PermissionHolderTest.java b/common/src/test/java/me/lucko/luckperms/common/model/PermissionHolderTest.java new file mode 100644 index 000000000..695c6691a --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/PermissionHolderTest.java @@ -0,0 +1,132 @@ +/* + * 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.model; + +import com.google.common.collect.ImmutableList; +import me.lucko.luckperms.common.event.EventDispatcher; +import me.lucko.luckperms.common.node.types.Permission; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import net.luckperms.api.model.data.DataMutateResult; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.model.data.TemporaryNodeMergeStrategy; +import net.luckperms.api.node.types.PermissionNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class PermissionHolderTest { + + @Mock private LuckPermsPlugin plugin; + + @BeforeEach + public void setupMocks() { + when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + } + + @Test + public void testTemporaryMergeNone() { + PermissionHolder holder = new Group("test", this.plugin); + + PermissionNode node1 = Permission.builder().permission("test").expiry(1, TimeUnit.HOURS).build(); + PermissionNode node2 = Permission.builder().permission("test").expiry(2, TimeUnit.HOURS).build(); + + TemporaryNodeMergeStrategy strategy = TemporaryNodeMergeStrategy.NONE; + + DataMutateResult.WithMergedNode r1 = holder.setNode(DataType.NORMAL, node1, strategy); + assertEquals(DataMutateResult.SUCCESS, r1.getResult()); + assertEquals(ImmutableList.of(node1), holder.normalData().asList()); + + DataMutateResult.WithMergedNode r2 = holder.setNode(DataType.NORMAL, node2, strategy); + assertEquals(DataMutateResult.FAIL_ALREADY_HAS, r2.getResult()); + assertEquals(ImmutableList.of(node1), holder.normalData().asList()); + } + + @Test + public void testTemporaryMergeReplaceIfLonger() { + PermissionHolder holder = new Group("test", this.plugin); + + PermissionNode node1 = Permission.builder().permission("test").expiry(1, TimeUnit.HOURS).build(); + PermissionNode node2 = Permission.builder().permission("test").expiry(2, TimeUnit.HOURS).build(); + + TemporaryNodeMergeStrategy strategy = TemporaryNodeMergeStrategy.REPLACE_EXISTING_IF_DURATION_LONGER; + + DataMutateResult.WithMergedNode r1 = holder.setNode(DataType.NORMAL, node1, strategy); + assertEquals(DataMutateResult.SUCCESS, r1.getResult()); + assertEquals(ImmutableList.of(node1), holder.normalData().asList()); + + DataMutateResult.WithMergedNode r2 = holder.setNode(DataType.NORMAL, node2, strategy); + assertEquals(DataMutateResult.SUCCESS, r2.getResult()); + assertEquals(ImmutableList.of(node2), holder.normalData().asList()); + assertEquals(node2, r2.getMergedNode()); + + DataMutateResult.WithMergedNode r3 = holder.setNode(DataType.NORMAL, node1, strategy); + assertEquals(DataMutateResult.FAIL_ALREADY_HAS, r3.getResult()); + assertEquals(ImmutableList.of(node2), holder.normalData().asList()); + } + + @Test + public void testTemporaryMergeAddDurations() { + PermissionHolder holder = new Group("test", this.plugin); + + PermissionNode node1 = Permission.builder().permission("test").expiry(2, TimeUnit.HOURS).build(); + PermissionNode node2 = Permission.builder().permission("test").expiry(1, TimeUnit.HOURS).build(); + + TemporaryNodeMergeStrategy strategy = TemporaryNodeMergeStrategy.ADD_NEW_DURATION_TO_EXISTING; + + DataMutateResult.WithMergedNode r1 = holder.setNode(DataType.NORMAL, node1, strategy); + assertEquals(DataMutateResult.SUCCESS, r1.getResult()); + assertEquals(ImmutableList.of(node1), holder.normalData().asList()); + + DataMutateResult.WithMergedNode r2 = holder.setNode(DataType.NORMAL, node2, strategy); + assertEquals(DataMutateResult.SUCCESS, r2.getResult()); + + Instant originalExpiry = node1.getExpiry(); + assertNotNull(originalExpiry); + + Instant newExpiry = r2.getMergedNode().getExpiry(); + assertNotNull(newExpiry); + + Instant expectedExpiry = node1.getExpiry().plus(1, ChronoUnit.HOURS); + + // uses wall-clock time, so allow for the tests to run slowly + assertTrue(Duration.between(newExpiry, expectedExpiry).abs().getSeconds() < 5); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/PrimaryGroupHolderTest.java b/common/src/test/java/me/lucko/luckperms/common/model/PrimaryGroupHolderTest.java new file mode 100644 index 000000000..f17df31b4 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/PrimaryGroupHolderTest.java @@ -0,0 +1,133 @@ +/* + * 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.model; + +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.graph.TraversalAlgorithm; +import me.lucko.luckperms.common.inheritance.InheritanceGraphFactory; +import me.lucko.luckperms.common.model.manager.group.GroupManager; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.common.node.types.Weight; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import net.luckperms.api.context.ContextSatisfyMode; +import net.luckperms.api.model.data.DataType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class PrimaryGroupHolderTest { + + @Mock private LuckPermsPlugin plugin; + @Mock private LuckPermsConfiguration configuration; + + private StandardGroupManager groupManager; + + @BeforeEach + public void setupMocks() { + this.groupManager = new StandardGroupManager(this.plugin); + + //noinspection unchecked,rawtypes + lenient().when(this.plugin.getGroupManager()).thenReturn((GroupManager) this.groupManager); + lenient().when(this.plugin.getInheritanceGraphFactory()).thenReturn(new InheritanceGraphFactory(this.plugin)); + lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration); + lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + lenient().when(this.configuration.get(ConfigKeys.CONTEXT_SATISFY_MODE)).thenReturn(ContextSatisfyMode.AT_LEAST_ONE_VALUE_PER_KEY); + lenient().when(this.configuration.get(ConfigKeys.GROUP_WEIGHTS)).thenReturn(Collections.emptyMap()); + lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION)).thenReturn(u -> null); + lenient().when(this.configuration.get(ConfigKeys.INHERITANCE_TRAVERSAL_ALGORITHM)).thenReturn(TraversalAlgorithm.BREADTH_FIRST); + } + + @Test + public void testStored() { + User user = new User(UUID.randomUUID(), this.plugin); + + // all holders inherit from stored, and should behave the same in the absence of any nodes + List holders = List.of( + new PrimaryGroupHolder.Stored(user), + new PrimaryGroupHolder.AllParentsByWeight(user), + new PrimaryGroupHolder.ParentsByWeight(user) + ); + + for (PrimaryGroupHolder holder : holders) { + // empty + assertEquals(Optional.empty(), holder.getStoredValue()); + assertNull(holder.calculateValue(QueryOptionsImpl.DEFAULT_CONTEXTUAL)); + + // set value + holder.setStoredValue("test"); + assertEquals(Optional.of("test"), holder.getStoredValue()); + assertEquals("test", holder.calculateValue(QueryOptionsImpl.DEFAULT_CONTEXTUAL)); + } + } + + @Test + public void testParentsByWeight() { + User user = new User(UUID.randomUUID(), this.plugin); + + Group special = createGroup("special", 100, null); // parent of mod, but not inherited directly + Group mod = createGroup("mod", 5, special); + Group admin = createGroup("admin", 10, mod); + + user.setNode(DataType.NORMAL, Inheritance.builder("mod").build(), false); + user.setNode(DataType.NORMAL, Inheritance.builder("admin").build(), false); + + PrimaryGroupHolder holder = new PrimaryGroupHolder.AllParentsByWeight(user); + assertEquals("special", holder.calculateValue(QueryOptionsImpl.DEFAULT_CONTEXTUAL)); + + holder = new PrimaryGroupHolder.ParentsByWeight(user); + assertEquals("admin", holder.calculateValue(QueryOptionsImpl.DEFAULT_CONTEXTUAL)); + } + + private Group createGroup(String name, int weight, Group parent) { + Group group = this.groupManager.getOrMake(name); + if (parent != null) { + group.normalData().add(Inheritance.builder().group(parent.getName()).build()); + } + group.normalData().add(Weight.builder().weight(weight).build()); + return group; + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/RecordedNodeMapTest.java b/common/src/test/java/me/lucko/luckperms/common/model/RecordedNodeMapTest.java new file mode 100644 index 000000000..4b137f443 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/RecordedNodeMapTest.java @@ -0,0 +1,147 @@ +/* + * 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.model; + +import me.lucko.luckperms.common.model.nodemap.NodeMapMutable; +import me.lucko.luckperms.common.model.nodemap.RecordedNodeMap; +import me.lucko.luckperms.common.node.factory.NodeBuilders; +import me.lucko.luckperms.common.util.Difference; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.types.InheritanceNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RecordedNodeMapTest { + + private static final PermissionHolderIdentifier ORIGIN = new PermissionHolderIdentifier(HolderType.GROUP, "test"); + + @Mock private PermissionHolder mockHolder; + + private NodeMapMutable delegate; + private RecordedNodeMap map; + + @BeforeEach + public void setup() { + when(this.mockHolder.getIdentifier()).thenReturn(ORIGIN); + this.delegate = new NodeMapMutable(this.mockHolder, DataType.NORMAL); + this.map = new RecordedNodeMap(this.delegate); + } + + private static Node makeNode(String key) { + return NodeBuilders.determineMostApplicable(key).build(); + } + + @Test + public void testMutationsAreRecorded() { + Node a = makeNode("a"); + Node b = makeNode("b"); + + this.map.add(a); + this.map.add(b); + this.map.remove(a); + + Difference exported = this.map.exportChanges(diff -> true); + assertEquals(Set.of(b), exported.getAdded()); + assertEquals(Set.of(), exported.getRemoved()); + } + + @Test + public void testExportChangesClearsTheLog() { + this.map.add(makeNode("a")); + + Difference first = this.map.exportChanges(diff -> true); + assertFalse(first.isEmpty()); + + // the log should have been reset, so a second export is empty + Difference second = this.map.exportChanges(diff -> true); + assertTrue(second.isEmpty()); + } + + @Test + public void testExportChangesRespectsPredicate() { + this.map.add(makeNode("a")); + + // predicate rejects the export, nothing should be returned and the log should be untouched + Difference rejected = this.map.exportChanges(diff -> false); + assertNull(rejected); + + Difference accepted = this.map.exportChanges(diff -> true); + assertFalse(accepted.isEmpty()); + } + + @Test + public void testDiscardChanges() { + this.map.add(makeNode("a")); + this.map.discardChanges(); + + Difference exported = this.map.exportChanges(diff -> true); + assertTrue(exported.isEmpty()); + + // the underlying delegate should be unaffected, only the change log is discarded + assertEquals(1, this.map.size()); + } + + @Test + public void testAddDefaultNodeToChangeSet() { + Difference result = this.map.addDefaultNodeToChangeSet(); + assertEquals(1, result.getAdded().size()); + Node added = result.getAdded().iterator().next(); + assertInstanceOf(InheritanceNode.class, added); + assertEquals("default", ((InheritanceNode) added).getGroupName()); + + // it should also have been recorded in the change log + Difference exported = this.map.exportChanges(diff -> true); + assertEquals(result.getAdded(), exported.getAdded()); + } + + @Test + public void testBypassMutatesWithoutRecording() { + assertSame(this.delegate, this.map.bypass()); + + this.map.bypass().add(makeNode("a")); + assertEquals(1, this.map.size()); + + // the mutation happened directly on the delegate, so it shouldn't show up in the change log + Difference exported = this.map.exportChanges(diff -> true); + assertTrue(exported == null || exported.isEmpty()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/TrackManagerTest.java b/common/src/test/java/me/lucko/luckperms/common/model/TrackManagerTest.java new file mode 100644 index 000000000..7f420e160 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/TrackManagerTest.java @@ -0,0 +1,51 @@ +/* + * 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.model; + +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.model.manager.track.StandardTrackManager; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +public class TrackManagerTest { + + @Mock private LuckPermsPlugin plugin; + + @Test + public void testSanitizeIdentifier() { + StandardTrackManager manager = new StandardTrackManager(this.plugin); + Track track = manager.getOrMake("TEST"); + assertEquals("test", track.getName()); + assertEquals(ImmutableSet.of("test"), manager.getAll().keySet()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/TrackTest.java b/common/src/test/java/me/lucko/luckperms/common/model/TrackTest.java new file mode 100644 index 000000000..68c412ecc --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/TrackTest.java @@ -0,0 +1,369 @@ +/* + * 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.model; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.LuckPermsConfiguration; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.event.EventDispatcher; +import me.lucko.luckperms.common.inheritance.InheritanceGraphFactory; +import me.lucko.luckperms.common.model.manager.group.GroupManager; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.util.Predicates; +import net.luckperms.api.model.data.DataMutateResult; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.track.DemotionResult; +import net.luckperms.api.track.PromotionResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class TrackTest { + + @Mock private LuckPermsPlugin plugin; + @Mock private LuckPermsConfiguration configuration; + + private StandardGroupManager groupManager; + + @BeforeEach + public void setupMocks() { + this.groupManager = new StandardGroupManager(this.plugin); + + //noinspection unchecked,rawtypes + lenient().when(this.plugin.getGroupManager()).thenReturn((GroupManager) this.groupManager); + lenient().when(this.plugin.getInheritanceGraphFactory()).thenReturn(new InheritanceGraphFactory(this.plugin)); + lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration); + lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION)).thenReturn(PrimaryGroupHolder.Stored::new); + } + + @Test + public void testGetRelative() { + Track track = new Track("test", this.plugin); + + Group a = this.groupManager.getOrMake("a"); + Group b = this.groupManager.getOrMake("b"); + Group c = this.groupManager.getOrMake("c"); + + track.setGroups(List.of("a", "b", "c")); + assertEquals(List.of("a", "b", "c"), track.getGroups()); + + // next + assertEquals("b", track.getNext(a)); + assertEquals("c", track.getNext(b)); + assertNull(track.getNext(c)); + assertThrows(IllegalArgumentException.class, () -> track.getNext("missing")); + + // previous + assertEquals("b", track.getPrevious(c)); + assertEquals("a", track.getPrevious(b)); + assertNull(track.getPrevious(a)); + assertThrows(IllegalArgumentException.class, () -> track.getPrevious("missing")); + } + + @Test + public void testAppendInsertRemove() { + Track track = new Track("test", this.plugin); + + Group a = this.groupManager.getOrMake("a"); + Group b = this.groupManager.getOrMake("b"); + Group c = this.groupManager.getOrMake("c"); + Group d = this.groupManager.getOrMake("d"); + + track.setGroups(List.of("a", "b")); + assertEquals(List.of("a", "b"), track.getGroups()); + + // append + DataMutateResult res = track.appendGroup(a); + assertEquals(DataMutateResult.FAIL_ALREADY_HAS, res); + + res = track.appendGroup(c); + assertEquals(DataMutateResult.SUCCESS, res); + assertEquals(List.of("a", "b", "c"), track.getGroups()); + + // insert + res = track.insertGroup(d, 1); + assertEquals(DataMutateResult.SUCCESS, res); + assertEquals(List.of("a", "d", "b", "c"), track.getGroups()); + + // remove + res = track.removeGroup(b); + assertEquals(DataMutateResult.SUCCESS, res); + assertEquals(List.of("a", "d", "c"), track.getGroups()); + } + + @Test + public void testPromoteThrowsOnSmallTrack() { + Track track = new Track("test", this.plugin); + track.setGroups(List.of("a")); + + User user = new User(UUID.randomUUID(), this.plugin); + assertThrows(IllegalStateException.class, () -> + track.promote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true) + ); + } + + @Test + public void testPromoteAddedToFirst() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + this.groupManager.getOrMake("b"); + track.setGroups(List.of("a", "b")); + + User user = new User(UUID.randomUUID(), this.plugin); + + // user isn't on the track at all, and addToFirst=false - no change made + PromotionResult result = track.promote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, false); + assertEquals(PromotionResult.Status.ADDED_TO_FIRST_GROUP, result.getStatus()); + assertTrue(result.getGroupTo().isEmpty()); + assertTrue(user.normalData().inheritanceNodesInContext(ImmutableContextSetImpl.EMPTY).isEmpty()); + + // user isn't on the track at all, and addToFirst=true - added to first group + result = track.promote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(PromotionResult.Status.ADDED_TO_FIRST_GROUP, result.getStatus()); + assertEquals("a", result.getGroupTo().orElse(null)); + assertTrue(userInheritsGroup(user, "a")); + } + + @Test + public void testPromoteSuccess() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + this.groupManager.getOrMake("b"); + this.groupManager.getOrMake("c"); + track.setGroups(List.of("a", "b", "c")); + + User user = new User(UUID.randomUUID(), this.plugin); + user.setNode(DataType.NORMAL, Inheritance.builder("a").build(), false); + + PromotionResult result = track.promote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(PromotionResult.Status.SUCCESS, result.getStatus()); + assertEquals("a", result.getGroupFrom().orElse(null)); + assertEquals("b", result.getGroupTo().orElse(null)); + assertFalse(userInheritsGroup(user, "a")); + assertTrue(userInheritsGroup(user, "b")); + + // promote again, to the end of the track + result = track.promote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(PromotionResult.Status.SUCCESS, result.getStatus()); + assertEquals("b", result.getGroupFrom().orElse(null)); + assertEquals("c", result.getGroupTo().orElse(null)); + assertFalse(userInheritsGroup(user, "b")); + assertTrue(userInheritsGroup(user, "c")); + + // already at the end of the track + result = track.promote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(PromotionResult.Status.END_OF_TRACK, result.getStatus()); + assertTrue(userInheritsGroup(user, "c")); + } + + @Test + public void testPromoteAmbiguousCall() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + this.groupManager.getOrMake("b"); + track.setGroups(List.of("a", "b")); + + User user = new User(UUID.randomUUID(), this.plugin); + user.setNode(DataType.NORMAL, Inheritance.builder("a").build(), false); + user.setNode(DataType.NORMAL, Inheritance.builder("b").build(), false); + + PromotionResult result = track.promote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(PromotionResult.Status.AMBIGUOUS_CALL, result.getStatus()); + } + + @Test + public void testPromoteMalformedTrack() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + // "b" is on the track but doesn't exist in the group manager + track.setGroups(List.of("a", "b")); + + User user = new User(UUID.randomUUID(), this.plugin); + user.setNode(DataType.NORMAL, Inheritance.builder("a").build(), false); + + PromotionResult result = track.promote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(PromotionResult.Status.MALFORMED_TRACK, result.getStatus()); + assertEquals("b", result.getGroupTo().orElse(null)); + assertFalse(userInheritsGroup(user, "b")); + } + + @Test + public void testPromoteUndefinedFailure() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + this.groupManager.getOrMake("b"); + track.setGroups(List.of("a", "b")); + + User user = new User(UUID.randomUUID(), this.plugin); + user.setNode(DataType.NORMAL, Inheritance.builder("a").build(), false); + + // permission checker denies the promotion + PromotionResult result = track.promote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysFalse(), null, true); + assertEquals(PromotionResult.Status.UNDEFINED_FAILURE, result.getStatus()); + assertTrue(userInheritsGroup(user, "a")); + assertFalse(userInheritsGroup(user, "b")); + } + + @Test + public void testDemoteThrowsOnSmallTrack() { + Track track = new Track("test", this.plugin); + track.setGroups(List.of("a")); + + User user = new User(UUID.randomUUID(), this.plugin); + assertThrows(IllegalStateException.class, () -> + track.demote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true) + ); + } + + @Test + public void testDemoteNotOnTrack() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + this.groupManager.getOrMake("b"); + track.setGroups(List.of("a", "b")); + + User user = new User(UUID.randomUUID(), this.plugin); + + DemotionResult result = track.demote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(DemotionResult.Status.NOT_ON_TRACK, result.getStatus()); + } + + @Test + public void testDemoteSuccess() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + this.groupManager.getOrMake("b"); + this.groupManager.getOrMake("c"); + track.setGroups(List.of("a", "b", "c")); + + User user = new User(UUID.randomUUID(), this.plugin); + user.setNode(DataType.NORMAL, Inheritance.builder("c").build(), false); + + DemotionResult result = track.demote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(DemotionResult.Status.SUCCESS, result.getStatus()); + assertEquals("c", result.getGroupFrom().orElse(null)); + assertEquals("b", result.getGroupTo().orElse(null)); + assertFalse(userInheritsGroup(user, "c")); + assertTrue(userInheritsGroup(user, "b")); + } + + @Test + public void testDemoteRemovedFromFirst() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + this.groupManager.getOrMake("b"); + track.setGroups(List.of("a", "b")); + + User user = new User(UUID.randomUUID(), this.plugin); + user.setNode(DataType.NORMAL, Inheritance.builder("a").build(), false); + + // removeFromFirst=false - no change made, but reports as if removed + DemotionResult result = track.demote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, false); + assertEquals(DemotionResult.Status.REMOVED_FROM_FIRST_GROUP, result.getStatus()); + assertNull(result.getGroupFrom().orElse(null)); + assertTrue(userInheritsGroup(user, "a")); + + // removeFromFirst=true - user is removed from the group entirely + result = track.demote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(DemotionResult.Status.REMOVED_FROM_FIRST_GROUP, result.getStatus()); + assertEquals("a", result.getGroupFrom().orElse(null)); + assertFalse(userInheritsGroup(user, "a")); + } + + @Test + public void testDemoteAmbiguousCall() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + this.groupManager.getOrMake("b"); + track.setGroups(List.of("a", "b")); + + User user = new User(UUID.randomUUID(), this.plugin); + user.setNode(DataType.NORMAL, Inheritance.builder("a").build(), false); + user.setNode(DataType.NORMAL, Inheritance.builder("b").build(), false); + + DemotionResult result = track.demote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(DemotionResult.Status.AMBIGUOUS_CALL, result.getStatus()); + } + + @Test + public void testDemoteMalformedTrack() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("b"); + // "a" is on the track but doesn't exist in the group manager + track.setGroups(List.of("a", "b")); + + User user = new User(UUID.randomUUID(), this.plugin); + user.setNode(DataType.NORMAL, Inheritance.builder("b").build(), false); + + DemotionResult result = track.demote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysTrue(), null, true); + assertEquals(DemotionResult.Status.MALFORMED_TRACK, result.getStatus()); + assertEquals("a", result.getGroupTo().orElse(null)); + } + + @Test + public void testDemoteUndefinedFailure() { + Track track = new Track("test", this.plugin); + this.groupManager.getOrMake("a"); + this.groupManager.getOrMake("b"); + track.setGroups(List.of("a", "b")); + + User user = new User(UUID.randomUUID(), this.plugin); + user.setNode(DataType.NORMAL, Inheritance.builder("b").build(), false); + + // permission checker denies the demotion + DemotionResult result = track.demote(user, ImmutableContextSetImpl.EMPTY, Predicates.alwaysFalse(), null, true); + assertEquals(DemotionResult.Status.UNDEFINED_FAILURE, result.getStatus()); + assertTrue(userInheritsGroup(user, "b")); + } + + private static boolean userInheritsGroup(User user, String group) { + for (InheritanceNode node : user.normalData().inheritanceNodesInContext(ImmutableContextSetImpl.EMPTY)) { + if (node.getGroupName().equalsIgnoreCase(group)) { + return true; + } + } + return false; + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/UserManagerTest.java b/common/src/test/java/me/lucko/luckperms/common/model/UserManagerTest.java new file mode 100644 index 000000000..14e3b34d8 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/UserManagerTest.java @@ -0,0 +1,113 @@ +/* + * 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.model; + +import com.google.common.collect.ImmutableList; +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.manager.user.StandardUserManager; +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class UserManagerTest { + + @Mock private LuckPermsPlugin plugin; + @Mock private LuckPermsBootstrap bootstrap; + @Mock private LuckPermsConfiguration configuration; + + @BeforeEach + public void setupMocks() { + 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"); + } + + @Test + public void testGiveDefaultIfNeeded() { + StandardUserManager manager = new StandardUserManager(this.plugin); + + User user = manager.getOrMake(UUID.randomUUID()); + assertEquals(ImmutableList.of(), user.normalData().asList()); + + boolean changed = manager.giveDefaultIfNeeded(user); + assertTrue(changed); + + Inheritance defaultNode = Inheritance.builder("default").build(); + assertEquals(ImmutableList.of(defaultNode), user.normalData().asList()); + + changed = manager.giveDefaultIfNeeded(user); + assertFalse(changed); + } + + @Test + public void testIsNonDefaultUser() { + StandardUserManager manager = new StandardUserManager(this.plugin); + User user = manager.getOrMake(UUID.randomUUID()); + manager.giveDefaultIfNeeded(user); + + assertFalse(manager.isNonDefaultUser(user)); + + user.normalData().add(Permission.builder().permission("test").build()); + assertTrue(manager.isNonDefaultUser(user)); + } + + @Test + public void testIsDefaultNode() { + StandardUserManager manager = new StandardUserManager(this.plugin); + + assertTrue(manager.isDefaultNode(Inheritance.builder().group("default").build())); + assertTrue(manager.isDefaultNode(Inheritance.builder().group("Default").build())); + + assertFalse(manager.isDefaultNode(Inheritance.builder().group("default").value(false).build())); + assertFalse(manager.isDefaultNode(Inheritance.builder().group("default").withContext("server", "test").build())); + assertFalse(manager.isDefaultNode(Inheritance.builder().group("default").expiry(1, TimeUnit.DAYS).build())); + assertFalse(manager.isDefaultNode(Inheritance.builder().group("test").build())); + assertFalse(manager.isDefaultNode(Permission.builder().permission("hello").build())); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/model/UserTest.java b/common/src/test/java/me/lucko/luckperms/common/model/UserTest.java new file mode 100644 index 000000000..e3da31ec5 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/model/UserTest.java @@ -0,0 +1,112 @@ +/* + * 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.model; + +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.plugin.LuckPermsPlugin; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class UserTest { + + @Mock private LuckPermsPlugin plugin; + @Mock private LuckPermsConfiguration configuration; + + @BeforeEach + public void setupMocks() { + lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + lenient().when(this.plugin.getConfiguration()).thenReturn(this.configuration); + lenient().when(this.configuration.get(ConfigKeys.PRIMARY_GROUP_CALCULATION)).thenReturn(u -> null); + } + + @Test + public void testGetDisplayName() { + UUID uniqueId = UUID.randomUUID(); + User user = new User(uniqueId, this.plugin); + + // no username set + assertEquals(uniqueId.toString(), user.getPlainDisplayName()); + + // username set + user.setUsername("Luck", true); + assertEquals("Luck", user.getPlainDisplayName()); + } + + @Test + public void testSetUsername() { + User user = new User(UUID.randomUUID(), this.plugin); + + // fail, over 16 chars + boolean res = user.setUsername("123456789123456789", false); + assertFalse(res); + assertEquals(Optional.empty(), user.getUsername()); + + // succeed - none set already + res = user.setUsername("luck", true); + assertTrue(res); + assertEquals(Optional.of("luck"), user.getUsername()); + + // fail - weak=true and username set already + res = user.setUsername("example", true); + assertFalse(res); + assertEquals(Optional.of("luck"), user.getUsername()); + + // succeed - weak=true and only change in case + res = user.setUsername("Luck", true); + assertFalse(res); + assertEquals(Optional.of("Luck"), user.getUsername()); + + // succeed - change in case + res = user.setUsername("LUCK", false); + assertFalse(res); + assertEquals(Optional.of("LUCK"), user.getUsername()); + + // succeed - change in value + res = user.setUsername("example", false); + assertTrue(res); + assertEquals(Optional.of("example"), user.getUsername()); + + // succeed - set to null + res = user.setUsername("", false); + assertTrue(res); + assertEquals(Optional.empty(), user.getUsername()); + } +} diff --git a/common/src/test/java/me/lucko/luckperms/common/node/NodeBuildersTest.java b/common/src/test/java/me/lucko/luckperms/common/node/NodeBuildersTest.java new file mode 100644 index 000000000..b46011aea --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/node/NodeBuildersTest.java @@ -0,0 +1,68 @@ +/* + * 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.node; + +import me.lucko.luckperms.common.node.factory.NodeBuilders; +import me.lucko.luckperms.common.node.types.Permission; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NodeBuildersTest { + + @ParameterizedTest + @CsvSource({ + "luckperms.user.info, Permission$Builder, PERMISSION", + "group.default, Inheritance$Builder, INHERITANCE", + "meta.key.value, Meta$Builder, META", + "prefix.100.hello, Prefix$Builder, PREFIX", + "suffix.100.hello, Suffix$Builder, SUFFIX", + "displayname.hello, DisplayName$Builder, DISPLAY_NAME", + "weight.10, Weight$Builder, WEIGHT", + "r=hello, RegexPermission$Builder, REGEX_PERMISSION", + "R=hello, RegexPermission$Builder, REGEX_PERMISSION" + }) + public void testDetermineMostApplicableType(String key, String expectedBuilderClass, String expectedType) { + NodeBuilder builder = NodeBuilders.determineMostApplicable(key); + assertTrue(builder.getClass().getName().endsWith(expectedBuilderClass)); + + Node node = builder.build(); + assertEquals(expectedType, node.getType().name()); + } + + @Test + public void testNonSpecificNodeBuild() { + Permission.Builder builder = Permission.builder().permission("group.default"); + assertThrows(IllegalArgumentException.class, builder::build); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/node/NodeCommandFactoryTest.java b/common/src/test/java/me/lucko/luckperms/common/node/NodeCommandFactoryTest.java new file mode 100644 index 000000000..5cc405bcb --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/node/NodeCommandFactoryTest.java @@ -0,0 +1,71 @@ +/* + * 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.node; + +import me.lucko.luckperms.common.model.HolderType; +import me.lucko.luckperms.common.node.factory.NodeCommandFactory; +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 net.luckperms.api.node.Node; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class NodeCommandFactoryTest { + + private static Stream testUndoCommand() { + return Stream.of( + Arguments.of("group test permission unset test", Permission.builder().permission("test").build(), HolderType.GROUP, false), + Arguments.of("user test permission unset test", Permission.builder().permission("test").build(), HolderType.USER, false), + Arguments.of("user test permission unsettemp test", Permission.builder().permission("test").expiry(1, TimeUnit.HOURS).build(), HolderType.USER, false), + Arguments.of("user test permission unset test server=foo world=bar", Permission.builder().permission("test").withContext("server", "foo").withContext("world", "bar").build(), HolderType.USER, false), + Arguments.of("user test permission unset test global", Permission.builder().permission("test").build(), HolderType.USER, true), + Arguments.of("user test parent remove test", Inheritance.builder().group("test").build(), HolderType.USER, false), + Arguments.of("user test parent removetemp test", Inheritance.builder().group("test").expiry(1, TimeUnit.HOURS).build(), HolderType.USER, false), + Arguments.of("user test meta removeprefix 100 test", Prefix.builder().priority(100).prefix("test").build(), HolderType.USER, false), + Arguments.of("user test meta removetempprefix 100 test", Prefix.builder().priority(100).prefix("test").expiry(1, TimeUnit.HOURS).build(), HolderType.USER, false), + Arguments.of("user test meta removeprefix 100 \"hello world\"", Prefix.builder().priority(100).prefix("hello world").build(), HolderType.USER, false), + Arguments.of("user test meta unset foo", Meta.builder().key("foo").value("bar").build(), HolderType.USER, false), + Arguments.of("user test meta unsettemp foo", Meta.builder().key("foo").value("bar").expiry(1, TimeUnit.HOURS).build(), HolderType.USER, false) + ); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource + public void testUndoCommand(String expected, Node node, HolderType holderType, boolean explicitGlobalContext) { + String result = NodeCommandFactory.undoCommand(node, "test", holderType, explicitGlobalContext); + assertEquals(expected, result); + + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/node/NodeComparatorTest.java b/common/src/test/java/me/lucko/luckperms/common/node/NodeComparatorTest.java new file mode 100644 index 000000000..f52f9b3b7 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/node/NodeComparatorTest.java @@ -0,0 +1,245 @@ +/* + * 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.node; + +import com.google.common.collect.Lists; +import me.lucko.luckperms.common.context.ContextSetComparatorTest; +import me.lucko.luckperms.common.node.comparator.NodeComparator; +import me.lucko.luckperms.common.node.comparator.NodeWithContextComparator; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.common.node.types.Permission; +import me.lucko.luckperms.common.node.types.Prefix; +import me.lucko.luckperms.common.node.types.Suffix; +import net.luckperms.api.node.Node; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for {@link NodeComparator} and {@link NodeWithContextComparator}. + * + *

    Both comparators are used to order nodes by "priority" - the primary use case being the + * {@code SortedSet}/{@code SortedMap} instances backing {@code NodeMapMutable}, where the + * descending comparator is used so that the highest priority node appears first when the + * collection is iterated.

    + * + *

    {@link ContextSetComparatorTest} already extensively covers the rules used to compare nodes + * based on their contexts (e.g. more specific contexts are higher priority) - this class instead + * focuses on the node-specific tie-breaking rules in {@link NodeComparator}, and how + * {@link NodeWithContextComparator} combines context specificity with those rules.

    + */ +public class NodeComparatorTest { + + @Nested + class NodeComparatorTests { + + @Test + @SuppressWarnings("EqualsWithItself") + public void testEquals() { + Node node = Permission.builder().permission("hello.world").build(); + assertEquals(0, NodeComparator.ascending().compare(node, node)); + assertEquals(0, NodeComparator.descending().compare(node, node)); + } + + @Test + public void testDescendingIsReverseOfAscending() { + Instant expiry = Instant.now().plusSeconds(60); + List nodes = new ArrayList<>(List.of( + Permission.builder().permission("a").build(), + Permission.builder().permission("z").build(), + Permission.builder().permission("a").expiry(expiry).build(), + Permission.builder().permission("foo.*").build(), + Permission.builder().permission("foo.bar.*").build() + )); + + List ascendingSorted = new ArrayList<>(nodes); + ascendingSorted.sort(NodeComparator.ascending()); + + List descendingSorted = new ArrayList<>(nodes); + descendingSorted.sort(NodeComparator.descending()); + + assertEquals(Lists.reverse(ascendingSorted), descendingSorted); + } + + @Test + public void testNonPermissionNodeTypesTakePriority() { + Node permissionNode = Permission.builder().permission("a").build(); + Node prefixNode = Prefix.builder("test", 100).build(); + Node suffixNode = Suffix.builder("test", 100).build(); + Node inheritNode = Inheritance.builder("test").build(); + + List list = new ArrayList<>(List.of(permissionNode, prefixNode, suffixNode, inheritNode)); + list.sort(NodeComparator.descending()); + assertEquals(List.of(inheritNode, prefixNode, suffixNode, permissionNode), list); + } + + @Test + public void testTemporaryNodesTakePriorityOverPermanent() { + Node permanentNode = Permission.builder().permission("hello.world").build(); + Instant expiry = Instant.now().plusSeconds(60); + Node temporaryNode = Permission.builder().permission("hello.world").expiry(expiry).build(); + + // the temporary node should sort first (higher priority) with the descending comparator + List list = new ArrayList<>(List.of(permanentNode, temporaryNode)); + list.sort(NodeComparator.descending()); + assertEquals(List.of(temporaryNode, permanentNode), list); + } + + @Test + public void testTemporaryNodesAreOrderedByExpiryTime() { + Instant expiry1 = Instant.now().plusSeconds(60); + Node temporaryNode1 = Permission.builder().permission("hello.world").expiry(expiry1).build(); + + Instant expiry2 = Instant.now().plusSeconds(120); + Node temporaryNode2 = Permission.builder().permission("hello.world").expiry(expiry2).build(); + + // the node with the sooner expiry time should sort first (higher priority) with the descending comparator + List list = new ArrayList<>(List.of(temporaryNode2, temporaryNode1)); + list.sort(NodeComparator.descending()); + assertEquals(List.of(temporaryNode1, temporaryNode2), list); + } + + @Test + public void testMoreSpecificWildcardsTakePriorityOverLessSpecific() { + Node rootWildcard = Permission.builder().permission("*").build(); + Node shallowWildcard = Permission.builder().permission("foo.*").build(); + Node deepWildcard = Permission.builder().permission("foo.bar.*").build(); + + List list = new ArrayList<>(List.of(rootWildcard, shallowWildcard, deepWildcard)); + list.sort(NodeComparator.descending()); + + // more specific (deeper) wildcards should be ordered first + assertEquals(List.of(deepWildcard, shallowWildcard, rootWildcard), list); + } + + @Test + public void testKeyOrderingIsAlphabeticalWhenOtherwiseEqual() { + Node a = Permission.builder().permission("a").build(); + Node m = Permission.builder().permission("m").build(); + Node z = Permission.builder().permission("z").build(); + + List list = new ArrayList<>(List.of(z, a, m)); + list.sort(NodeComparator.descending()); + + assertEquals(List.of(a, m, z), list); + } + + @Test + public void testFalseValuePriorityOverTrue() { + Node trueNode = Permission.builder().permission("hello.world").value(true).build(); + Node falseNode = Permission.builder().permission("hello.world").value(false).build(); + + List list = new ArrayList<>(List.of(trueNode, falseNode)); + list.sort(NodeComparator.descending()); + + assertEquals(List.of(falseNode, trueNode), list); + } + } + + @Nested + class NodeWithContextComparatorTests { + + @Test + @SuppressWarnings("EqualsWithItself") + public void testEquals() { + Node node = Permission.builder().permission("hello.world").withContext("server", "foo").build(); + assertEquals(0, NodeWithContextComparator.ascending().compare(node, node)); + assertEquals(0, NodeWithContextComparator.descending().compare(node, node)); + } + + @Test + public void testDescendingIsReverseOfAscending() { + Instant expiry = Instant.now().plusSeconds(60); + List nodes = new ArrayList<>(List.of( + Permission.builder().permission("a").build(), + Permission.builder().permission("a").withContext("server", "foo").build(), + Permission.builder().permission("a").withContext("world", "foo").build(), + Permission.builder().permission("a").expiry(expiry).build() + )); + + List ascendingSorted = new ArrayList<>(nodes); + ascendingSorted.sort(NodeWithContextComparator.ascending()); + + List descendingSorted = new ArrayList<>(nodes); + descendingSorted.sort(NodeWithContextComparator.descending()); + + assertEquals(Lists.reverse(ascendingSorted), descendingSorted); + } + + @Test + public void testContextSpecificityTakesPriorityOverNodeProperties() { + // this node has a more specific context set, but would otherwise be considered + // lower priority than the other node if only NodeComparator rules were applied + // (it's permanent, whereas the other is temporary) + Node moreSpecificContext = Permission.builder().permission("hello.world").withContext("server", "foo").build(); + Instant expiry = Instant.now().plusSeconds(60); + Node lessSpecificContext = Permission.builder().permission("hello.world").expiry(expiry).build(); + + // context specificity should win out regardless of the temporary/permanent difference + assertTrue(NodeWithContextComparator.descending().compare(moreSpecificContext, lessSpecificContext) < 0); + + List list = new ArrayList<>(List.of(lessSpecificContext, moreSpecificContext)); + list.sort(NodeWithContextComparator.descending()); + assertEquals(List.of(moreSpecificContext, lessSpecificContext), list); + } + + @Test + public void testFallsBackToNodeComparatorWhenContextsAreEqual() { + // both nodes share the same (empty) context set, so the comparator should + // fall back to NodeComparator's rules - temporary nodes take priority + Node permanentNode = Permission.builder().permission("hello.world").build(); + Instant expiry = Instant.now().plusSeconds(60); + Node temporaryNode = Permission.builder().permission("hello.world").expiry(expiry).build(); + + List list = new ArrayList<>(List.of(permanentNode, temporaryNode)); + list.sort(NodeWithContextComparator.descending()); + assertEquals(List.of(temporaryNode, permanentNode), list); + } + + @Test + public void testPriorityOrderingAcrossContextsAndNodeProperties() { + Node empty = Permission.builder().permission("hello.world").build(); + Node withServerContext = Permission.builder().permission("hello.world").withContext("server", "foo").build(); + Instant expiry = Instant.now().plusSeconds(60); + Node withServerContextTemporary = (Permission.builder().permission("hello.world").expiry(expiry).build()).toBuilder() + .withContext("server", "foo") + .build(); + + List list = new ArrayList<>(List.of(empty, withServerContext, withServerContextTemporary)); + list.sort(NodeWithContextComparator.descending()); + + // most specific context wins first; within the same context, temporary beats permanent + assertEquals(List.of(withServerContextTemporary, withServerContext, empty), list); + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/node/NodeEqualityTest.java b/common/src/test/java/me/lucko/luckperms/common/node/NodeEqualityTest.java new file mode 100644 index 000000000..3de8ffc8d --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/node/NodeEqualityTest.java @@ -0,0 +1,224 @@ +/* + * 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.node; + +import me.lucko.luckperms.common.node.types.Permission; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeEqualityPredicate; +import net.luckperms.api.node.types.PermissionNode; +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.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NodeEqualityTest { + + private static Node node(String permission, boolean value, Instant expiry, String contextKey, String contextValue) { + PermissionNode.Builder builder = Permission.builder().permission(permission).value(value); + if (expiry != null) { + builder.expiry(expiry); + } + if (contextKey != null) { + builder.withContext(contextKey, contextValue); + } + return builder.build(); + } + + @Test + public void testExact() { + Instant expiry = Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.SECONDS); + + Node base = node("hello.world", true, expiry, "server", "one"); + Node same = node("hello.world", true, expiry, "server", "one"); + Node differentKey = node("hello.other", true, expiry, "server", "one"); + Node differentValue = node("hello.world", false, expiry, "server", "one"); + Node differentExpiry = node("hello.world", true, expiry.plusSeconds(60), "server", "one"); + Node differentContext = node("hello.world", true, expiry, "server", "two"); + + NodeEqualityPredicate predicate = NodeEqualityPredicate.EXACT; + assertTrue(predicate.areEqual(base, same)); + assertFalse(predicate.areEqual(base, differentKey)); + assertFalse(predicate.areEqual(base, differentValue)); + assertFalse(predicate.areEqual(base, differentExpiry)); + assertFalse(predicate.areEqual(base, differentContext)); + } + + @Test + public void testIgnoreValue() { + Instant expiry = Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.SECONDS); + + Node base = node("hello.world", true, expiry, "server", "one"); + Node differentValue = node("hello.world", false, expiry, "server", "one"); + Node differentKey = node("hello.other", true, expiry, "server", "one"); + Node differentExpiry = node("hello.world", true, expiry.plusSeconds(60), "server", "one"); + Node differentContext = node("hello.world", true, expiry, "server", "two"); + + NodeEqualityPredicate predicate = NodeEqualityPredicate.IGNORE_VALUE; + // value is ignored + assertTrue(predicate.areEqual(base, differentValue)); + assertFalse(predicate.areEqual(base, differentKey)); + assertFalse(predicate.areEqual(base, differentExpiry)); + assertFalse(predicate.areEqual(base, differentContext)); + } + + @Test + public void testIgnoreExpiryTime() { + Instant expiryA = Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.SECONDS); + Instant expiryB = Instant.now().plusSeconds(120).truncatedTo(ChronoUnit.SECONDS); + + Node base = node("hello.world", true, expiryA, "server", "one"); + Node differentExpiryTime = node("hello.world", true, expiryB, "server", "one"); + Node permanent = node("hello.world", true, null, "server", "one"); + Node differentValue = node("hello.world", false, expiryA, "server", "one"); + Node differentContext = node("hello.world", true, expiryA, "server", "two"); + + NodeEqualityPredicate predicate = NodeEqualityPredicate.IGNORE_EXPIRY_TIME; + // exact expiry time is ignored, but whether it has an expiry still matters + assertTrue(predicate.areEqual(base, differentExpiryTime)); + assertFalse(predicate.areEqual(base, permanent)); + assertFalse(predicate.areEqual(base, differentValue)); + assertFalse(predicate.areEqual(base, differentContext)); + } + + @Test + public void testIgnoreExpiryTimeAndValue() { + Instant expiryA = Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.SECONDS); + Instant expiryB = Instant.now().plusSeconds(120).truncatedTo(ChronoUnit.SECONDS); + + Node base = node("hello.world", true, expiryA, "server", "one"); + Node differentValue = node("hello.world", false, expiryA, "server", "one"); + Node differentExpiryTime = node("hello.world", true, expiryB, "server", "one"); + Node permanent = node("hello.world", true, null, "server", "one"); + Node differentContext = node("hello.world", true, expiryA, "server", "two"); + + NodeEqualityPredicate predicate = NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE; + // value and exact expiry time are ignored, but whether it has an expiry still matters + assertTrue(predicate.areEqual(base, differentValue)); + assertTrue(predicate.areEqual(base, differentExpiryTime)); + assertFalse(predicate.areEqual(base, permanent)); + assertFalse(predicate.areEqual(base, differentContext)); + } + + @Test + public void testIgnoreValueOrIfTemporary() { + Instant expiry = Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.SECONDS); + + Node base = node("hello.world", true, expiry, "server", "one"); + Node differentValue = node("hello.world", false, expiry, "server", "one"); + Node permanent = node("hello.world", true, null, "server", "one"); + Node differentContext = node("hello.world", true, expiry, "server", "two"); + Node differentKey = node("hello.other", true, expiry, "server", "one"); + + NodeEqualityPredicate predicate = NodeEqualityPredicate.IGNORE_VALUE_OR_IF_TEMPORARY; + // value and expiry (whether temporary or not) are entirely ignored + assertTrue(predicate.areEqual(base, differentValue)); + assertTrue(predicate.areEqual(base, permanent)); + assertFalse(predicate.areEqual(base, differentContext)); + assertFalse(predicate.areEqual(base, differentKey)); + } + + @Test + public void testOnlyKey() { + Node base = node("hello.world", true, null, "server", "one"); + Node differentValue = node("hello.world", false, null, "server", "two"); + Node differentKey = node("hello.other", true, null, "server", "one"); + + NodeEqualityPredicate predicate = NodeEqualityPredicate.ONLY_KEY; + assertTrue(predicate.areEqual(base, differentValue)); + assertFalse(predicate.areEqual(base, differentKey)); + } + + private static Stream testSameInstanceAlwaysEqual() { + return Stream.of( + Arguments.of(NodeEqualityPredicate.EXACT), + Arguments.of(NodeEqualityPredicate.ONLY_KEY), + Arguments.of(NodeEqualityPredicate.IGNORE_VALUE), + Arguments.of(NodeEqualityPredicate.IGNORE_EXPIRY_TIME), + Arguments.of(NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE), + Arguments.of(NodeEqualityPredicate.IGNORE_VALUE_OR_IF_TEMPORARY) + ); + } + + @ParameterizedTest + @MethodSource + public void testSameInstanceAlwaysEqual(NodeEqualityPredicate predicate) { + Instant expiry = Instant.now().plusSeconds(60).truncatedTo(ChronoUnit.SECONDS); + Node node = node("hello.world", true, expiry, "server", "one"); + + assertTrue(predicate.areEqual(node, node)); + } + + @ParameterizedTest + @EnumSource + public void testComparesContexts(NodeEquality nodeEquality) { + if (nodeEquality.name().contains("CONTEXTS")) { + assertTrue(nodeEquality.comparesContexts()); + } else { + assertFalse(nodeEquality.comparesContexts()); + } + } + + private static Stream testMappingToNodeEquality() { + return Stream.of( + Arguments.of(NodeEqualityPredicate.EXACT, NodeEquality.KEY_VALUE_EXPIRY_CONTEXTS), + Arguments.of(NodeEqualityPredicate.IGNORE_VALUE, NodeEquality.KEY_EXPIRY_CONTEXTS), + Arguments.of(NodeEqualityPredicate.IGNORE_EXPIRY_TIME, NodeEquality.KEY_VALUE_HASEXPIRY_CONTEXTS), + Arguments.of(NodeEqualityPredicate.IGNORE_EXPIRY_TIME_AND_VALUE, NodeEquality.KEY_HASEXPIRY_CONTEXTS), + Arguments.of(NodeEqualityPredicate.IGNORE_VALUE_OR_IF_TEMPORARY, NodeEquality.KEY_CONTEXTS), + Arguments.of(NodeEqualityPredicate.ONLY_KEY, NodeEquality.KEY) + ); + } + + @ParameterizedTest + @MethodSource + public void testMappingToNodeEquality(NodeEqualityPredicate predicate, NodeEquality expected) { + assertEquals(expected, NodeEquality.of(predicate)); + } + + @Test + public void testMappingToNodeEqualityNull() { + assertNull(NodeEquality.of((o1, o2) -> true)); + } + + @Test + public void testComparesContextsHelper() { + assertTrue(NodeEquality.comparesContexts(NodeEqualityPredicate.EXACT)); + assertFalse(NodeEquality.comparesContexts(NodeEqualityPredicate.ONLY_KEY)); + // an unrecognised predicate maps to null, and should be reported as not comparing contexts + assertFalse(NodeEquality.comparesContexts((o1, o2) -> true)); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/node/NodeMatcherTest.java b/common/src/test/java/me/lucko/luckperms/common/node/NodeMatcherTest.java new file mode 100644 index 000000000..0db4e39af --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/node/NodeMatcherTest.java @@ -0,0 +1,269 @@ +/* + * 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.node; + +import me.lucko.luckperms.common.filter.Comparison; +import me.lucko.luckperms.common.filter.Constraint; +import me.lucko.luckperms.common.node.matcher.ConstraintNodeMatcher; +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.node.types.Weight; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.NodeEqualityPredicate; +import net.luckperms.api.node.NodeType; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.node.types.MetaNode; +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.CsvSource; +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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class NodeMatcherTest { + + @Test + public void testKey() { + ConstraintNodeMatcher matcher = StandardNodeMatchers.key("foo"); + Constraint constraint = matcher.getConstraint(); + + assertEquals(Comparison.EQUAL, constraint.comparison()); + assertEquals("foo", constraint.value()); + + ConstraintNodeMatcher typedMatcher = StandardNodeMatchers.key(Permission.builder().permission("foo").build()); + Constraint typedConstraint = typedMatcher.getConstraint(); + + assertEquals(Comparison.EQUAL, typedConstraint.comparison()); + assertEquals("foo", typedConstraint.value()); + } + + @ParameterizedTest + @CsvSource({ + "foo, foo%", + }) + public void testKeyStartsWith(String value, String expectedConstraint) { + ConstraintNodeMatcher matcher = StandardNodeMatchers.keyStartsWith(value); + Constraint constraint = matcher.getConstraint(); + + assertEquals(Comparison.SIMILAR, constraint.comparison()); + assertEquals(expectedConstraint, constraint.value()); + } + + @ParameterizedTest + @CsvSource({ + "foo, meta.foo.%", + }) + public void testMetaKey(String value, String expectedConstraint) { + ConstraintNodeMatcher matcher = StandardNodeMatchers.metaKey(value); + Constraint constraint = matcher.getConstraint(); + + assertEquals(Comparison.SIMILAR, constraint.comparison()); + assertEquals(expectedConstraint, constraint.value()); + } + + + private static Stream testType() { + return Stream.of( + Arguments.of(NodeType.REGEX_PERMISSION, "r=%"), + Arguments.of(NodeType.INHERITANCE, "group.%"), + Arguments.of(NodeType.PREFIX, "prefix.%.%"), + Arguments.of(NodeType.SUFFIX, "suffix.%.%"), + Arguments.of(NodeType.META, "meta.%.%"), + Arguments.of(NodeType.WEIGHT, "weight.%"), + Arguments.of(NodeType.DISPLAY_NAME, "displayname.%") + ); + } + + @ParameterizedTest + @MethodSource + public void testType(NodeType type, String expectedValue) { + ConstraintNodeMatcher matcher = StandardNodeMatchers.type(type); + Constraint constraint = matcher.getConstraint(); + + assertEquals(Comparison.SIMILAR, constraint.comparison()); + assertEquals(expectedValue, constraint.value()); + + } + + @ParameterizedTest + @CsvSource({ + "foo, true", + "bar, false", + "FOO, true", // case-insensitive + }) + public void testKeyMatch(String permission, boolean expectedMatch) { + ConstraintNodeMatcher matcher = StandardNodeMatchers.key("foo"); + Node node = Permission.builder().permission(permission).build(); + + assertEquals(expectedMatch, matcher.test(node)); + if (expectedMatch) { + assertSame(node, matcher.match(node)); + } else { + assertNull(matcher.match(node)); + } + } + + @ParameterizedTest + @CsvSource({ + "foo.bar, true", + "foo.baz, true", + "foo, false", + "bar.foo, false", + }) + public void testKeyStartsWithMatch(String permission, boolean expectedMatch) { + ConstraintNodeMatcher matcher = StandardNodeMatchers.keyStartsWith("foo."); + Node node = Permission.builder().permission(permission).build(); + assertEquals(expectedMatch, matcher.test(node)); + } + + private static Stream testNodeEqualsMatch() { + return Stream.of( + // same key & value -> matches + Arguments.of( + Permission.builder().permission("foo").value(true).build(), + Permission.builder().permission("foo").value(true).build(), + NodeEqualityPredicate.EXACT, + true + ), + // different value, EXACT equality -> doesn't match + Arguments.of( + Permission.builder().permission("foo").value(true).build(), + Permission.builder().permission("foo").value(false).build(), + NodeEqualityPredicate.EXACT, + false + ), + // different value, but IGNORE_VALUE equality -> matches + Arguments.of( + Permission.builder().permission("foo").value(true).build(), + Permission.builder().permission("foo").value(false).build(), + NodeEqualityPredicate.IGNORE_VALUE, + true + ), + // different key -> never matches, regardless of equality predicate + Arguments.of( + Permission.builder().permission("foo").build(), + Permission.builder().permission("bar").build(), + NodeEqualityPredicate.IGNORE_VALUE, + false + ), + // ONLY_KEY equality -> value/context/expiry differences are ignored + Arguments.of( + Permission.builder().permission("foo").value(true).withContext("server", "foo").build(), + Permission.builder().permission("foo").value(false).build(), + NodeEqualityPredicate.ONLY_KEY, + true + ) + ); + } + + @ParameterizedTest + @MethodSource + public void testNodeEqualsMatch(Node matcherNode, Node candidateNode, NodeEqualityPredicate equalityPredicate, boolean expectedMatch) { + ConstraintNodeMatcher matcher = StandardNodeMatchers.equals(matcherNode, equalityPredicate); + + assertEquals(expectedMatch, matcher.test(candidateNode)); + assertEquals(expectedMatch, matcher.match(candidateNode) != null); + } + + @Test + public void testNodeEqualsMatchFailsConstraintBeforeReachingFilter() { + // the key comparison constraint is checked before filterConstraintMatch is invoked + // if the keys don't match, filterConstraintMatch should never even be considered + ConstraintNodeMatcher matcher = StandardNodeMatchers.equals( + Permission.builder().permission("foo").build(), + (o1, o2) -> { + throw new RuntimeException("should never be called"); + } + ); + + Node differentKeyNode = Permission.builder().permission("bar").build(); + assertNull(matcher.match(differentKeyNode)); + assertFalse(matcher.test(differentKeyNode)); + } + + @ParameterizedTest + @CsvSource({ + "foo, true", + "bar, false", + }) + public void testMetaKeyMatch(String metaKey, boolean expectedMatch) { + ConstraintNodeMatcher matcher = StandardNodeMatchers.metaKey("foo"); + Node node = Meta.builder(metaKey, "100").build(); + + assertEquals(expectedMatch, matcher.test(node)); + + MetaNode result = matcher.match(node); + if (expectedMatch) { + assertNotNull(result); + assertSame(node, result); + assertEquals(metaKey, result.getMetaKey()); + } else { + assertNull(result); + } + } + + private static Stream testTypeMatch() { + return Stream.of( + Arguments.of(NodeType.INHERITANCE, Inheritance.builder("admin").build(), true), + Arguments.of(NodeType.INHERITANCE, Permission.builder().permission("foo").build(), false), + Arguments.of(NodeType.META, Meta.builder("foo", "bar").build(), true), + Arguments.of(NodeType.META, Prefix.builder("foo", 100).build(), false), + Arguments.of(NodeType.PREFIX, Prefix.builder("foo", 100).build(), true), + Arguments.of(NodeType.PREFIX, Meta.builder("foo", "bar").build(), false), + Arguments.of(NodeType.WEIGHT, Weight.builder(100).build(), true), + Arguments.of(NodeType.WEIGHT, Permission.builder().permission("some.permission").build(), false) + ); + } + + @ParameterizedTest + @MethodSource + public void testTypeMatch(NodeType type, Node node, boolean expectedMatch) { + ConstraintNodeMatcher matcher = StandardNodeMatchers.type(type); + + assertEquals(expectedMatch, matcher.test(node)); + assertEquals(expectedMatch, matcher.match(node) != null); + } + + @Test + public void testTypeMatchCastsToRequestedType() { + ConstraintNodeMatcher matcher = StandardNodeMatchers.type(NodeType.INHERITANCE); + Node node = Inheritance.builder("admin").build(); + + InheritanceNode result = matcher.match(node); + assertNotNull(result); + assertSame(node, result); + } +} diff --git a/common/src/test/java/me/lucko/luckperms/common/node/NodeParseTest.java b/common/src/test/java/me/lucko/luckperms/common/node/NodeParseTest.java new file mode 100644 index 000000000..bf392abf4 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/node/NodeParseTest.java @@ -0,0 +1,276 @@ +/* + * 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.node; + +import me.lucko.luckperms.common.node.types.DisplayName; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.common.node.types.Meta; +import me.lucko.luckperms.common.node.types.Prefix; +import me.lucko.luckperms.common.node.types.RegexPermission; +import me.lucko.luckperms.common.node.types.Suffix; +import me.lucko.luckperms.common.node.types.Weight; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class NodeParseTest { + + @ParameterizedTest + @CsvSource({ + "group.test, test", + "group.TEST, test", + }) + public void testInheritance(String key, String expectedGroupName) { + Inheritance.Builder builder = Inheritance.parse(key); + assertNotNull(builder); + + Inheritance node = builder.build(); + assertEquals(expectedGroupName, node.getGroupName()); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "aaaa" + }) + public void testInheritanceFail(String key) { + Inheritance.Builder builder = Inheritance.parse(key); + assertNull(builder); + } + + @ParameterizedTest + @ValueSource(strings = { + "group.hello world" + }) + public void testInheritanceThrows(String key) { + assertThrows(IllegalArgumentException.class, () -> Inheritance.parse(key)); + } + + @ParameterizedTest + @CsvSource({ + "displayname.test, test", + "displayname.TEST, TEST", + "displayname.hello world, hello world" + }) + public void testDisplayName(String key, String expectedDisplayName) { + DisplayName.Builder builder = DisplayName.parse(key); + assertNotNull(builder); + + DisplayName node = builder.build(); + assertEquals(expectedDisplayName, node.getDisplayName()); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "aaaa" + }) + public void testDisplayNameFail(String key) { + DisplayName.Builder builder = DisplayName.parse(key); + assertNull(builder); + } + + @ParameterizedTest + @ValueSource(strings = { + "displayname." + }) + public void testDisplayNameThrows(String key) { + assertThrows(IllegalArgumentException.class, () -> DisplayName.parse(key)); + } + + @ParameterizedTest + @CsvSource({ + "weight.100, 100", + "weight.-100, -100", + "weight.0, 0" + }) + public void testWeight(String key, int expectedWeight) { + Weight.Builder builder = Weight.parse(key); + assertNotNull(builder); + + Weight node = builder.build(); + assertEquals(expectedWeight, node.getWeight()); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "aaaa", + "weight.", + "weight.hello" + }) + public void testWeightFail(String key) { + Weight.Builder builder = Weight.parse(key); + assertNull(builder); + } + + @ParameterizedTest + @CsvSource({ + "prefix.100.hello, 100, hello", + "prefix.-100.hello, -100, hello", + "prefix.0.hello, 0, hello", + "prefix.100.hello world, 100, hello world", + "prefix.100.HELLO world &123, 100, HELLO world &123", + "prefix.100., 100, ''", + "prefix.100.hello\\.world, 100, hello.world", + "prefix.100.hello.world, 100, hello.world", + }) + public void testPrefix(String key, int expectedPriority, String expectedValue) { + Prefix.Builder builder = Prefix.parse(key); + assertNotNull(builder); + + Prefix node = builder.build(); + assertEquals(expectedPriority, node.getPriority()); + assertEquals(expectedValue, node.getMetaValue()); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "aaaa", + "prefix.", + "prefix.hello", + "prefix.100", + "prefix.hello.hello", + "suffix.100.hello" + }) + public void testPrefixFail(String key) { + Prefix.Builder builder = Prefix.parse(key); + assertNull(builder); + } + + @ParameterizedTest + @CsvSource({ + "suffix.100.hello, 100, hello", + "suffix.-100.hello, -100, hello", + "suffix.0.hello, 0, hello", + "suffix.100.hello world, 100, hello world", + "suffix.100.HELLO world &123, 100, HELLO world &123", + "suffix.100., 100, ''", + "suffix.100.hello\\.world, 100, hello.world", + "suffix.100.hello.world, 100, hello.world", + }) + public void testSuffix(String key, int expectedPriority, String expectedValue) { + Suffix.Builder builder = Suffix.parse(key); + assertNotNull(builder); + + Suffix node = builder.build(); + assertEquals(expectedPriority, node.getPriority()); + assertEquals(expectedValue, node.getMetaValue()); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "aaaa", + "suffix.", + "suffix.hello", + "suffix.100", + "suffix.hello.hello", + "prefix.100.hello" + }) + public void testSuffixFail(String key) { + Suffix.Builder builder = Suffix.parse(key); + assertNull(builder); + } + + @ParameterizedTest + @CsvSource({ + "meta.k.v, k, v", + "meta.hello.world, hello, world", + "meta.hello., hello, ''", + "meta.a\\.b.hel\\.lo, a.b, hel.lo", + "meta.a\\\\.b.hel\\.lo, a\\.b, hel.lo", + "meta.a.b.c, a, b.c" + }) + public void testMeta(String key, String expectedKey, String expectedValue) { + Meta.Builder builder = Meta.parse(key); + assertNotNull(builder); + + Meta node = builder.build(); + assertEquals(expectedKey, node.getMetaKey()); + assertEquals(expectedValue, node.getMetaValue()); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "aaaa", + "meta.", + "meta.hello", + }) + public void testMetaFail(String key) { + Meta.Builder builder = Meta.parse(key); + assertNull(builder); + } + + @ParameterizedTest + @ValueSource(strings = { + "meta.." + }) + public void testMetaFailThrows(String key) { + assertThrows(IllegalArgumentException.class, () -> Meta.parse(key)); + } + + @ParameterizedTest + @CsvSource({ + "r=hello, hello", + "R=hello, hello", + "r=.*&^12 3[], .*&^12 3[]" + }) + public void testRegexPermission(String key, String expectedPattern) { + RegexPermission.Builder builder = RegexPermission.parse(key); + assertNotNull(builder); + + RegexPermission node = builder.build(); + assertEquals(expectedPattern, node.getPatternString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "aaaa" + }) + public void testRegexPermissionFail(String key) { + RegexPermission.Builder builder = RegexPermission.parse(key); + assertNull(builder); + } + + @ParameterizedTest + @ValueSource(strings = { + "r=", + "R=" + }) + public void testRegexPermissionFailThrows(String key) { + assertThrows(IllegalArgumentException.class, () -> RegexPermission.parse(key)); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/node/NodeTest.java b/common/src/test/java/me/lucko/luckperms/common/node/NodeTest.java new file mode 100644 index 000000000..203c9e73c --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/node/NodeTest.java @@ -0,0 +1,152 @@ +/* + * 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.node; + +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +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.node.types.Suffix; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.metadata.NodeMetadataKey; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; + +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NodeTest { + + @Test + public void testBasic() { + Node node = Permission.builder() + .permission("hello.world") + .build(); + + assertEquals("hello.world", node.getKey()); + assertTrue(node.getValue()); + assertNull(node.getExpiry()); + assertNull(node.getExpiryDuration()); + assertFalse(node.hasExpired()); + assertFalse(node.hasExpiry()); + } + + @Test + public void testEscaping() { + Node node = Meta.builder("hel.lo", "wo.rld").build(); + assertEquals("meta.hel\\.lo.wo\\.rld", node.getKey()); + + node = Prefix.builder("hel.lo", 100).build(); + assertEquals("prefix.100.hel\\.lo", node.getKey()); + + node = Suffix.builder("hel.lo", 100).build(); + assertEquals("suffix.100.hel\\.lo", node.getKey()); + } + + @Test + public void testExpiry() { + Instant instant = Instant.now() + .plusSeconds(60) + .truncatedTo(ChronoUnit.SECONDS); + + Node node = Permission.builder() + .permission("hello.world") + .expiry(instant) + .build(); + + assertEquals("hello.world", node.getKey()); + assertTrue(node.getValue()); + assertEquals(instant, node.getExpiry()); + assertNotNull(node.getExpiryDuration()); + assertFalse(node.hasExpired()); + assertTrue(node.hasExpiry()); + } + + @Test + public void testContext() { + Node node = Permission.builder() + .permission("hello.world") + .withContext("hello", "123") + .withContext("world", "456") + .build(); + + ImmutableContextSet contexts = node.getContexts(); + ImmutableContextSet expected = new ImmutableContextSetImpl.BuilderImpl() + .add("hello", "123") + .add("world", "456") + .build(); + + assertEquals(expected, contexts); + } + + @Test + public void testMetadata() { + NodeMetadataKey key = NodeMetadataKey.of("test2", UUID.class); + UUID randomUniqueId = UUID.randomUUID(); + + Node node = Permission.builder() + .permission("hello.world") + .withMetadata(key, randomUniqueId) + .build(); + + assertEquals(Optional.of(randomUniqueId), node.getMetadata(key)); + assertEquals(randomUniqueId, node.metadata(key)); + } + + @Test + public void testMetadataFails() { + NodeMetadataKey key1 = NodeMetadataKey.of("test1", String.class); + NodeMetadataKey key2 = NodeMetadataKey.of("test2", UUID.class); + NodeMetadataKey key3 = NodeMetadataKey.of("test2", UUID.class); + NodeMetadataKey key4 = NodeMetadataKey.of("test2", String.class); + UUID randomUniqueId = UUID.randomUUID(); + + Node node = Permission.builder() + .permission("hello.world") + .withMetadata(key2, randomUniqueId) + .build(); + + assertEquals(Optional.of(randomUniqueId), node.getMetadata(key2)); + assertEquals(Optional.of(randomUniqueId), node.getMetadata(key3)); + assertEquals(randomUniqueId, node.metadata(key2)); + assertEquals(randomUniqueId, node.metadata(key3)); + + assertEquals(Optional.empty(), node.getMetadata(key1)); + assertEquals(Optional.empty(), node.getMetadata(key4)); + assertThrows(IllegalStateException.class, () -> node.metadata(key1)); + assertThrows(IllegalStateException.class, () -> node.metadata(key4)); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/node/ShorthandParserTest.java b/common/src/test/java/me/lucko/luckperms/common/node/ShorthandParserTest.java new file mode 100644 index 000000000..3422db4cd --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/node/ShorthandParserTest.java @@ -0,0 +1,91 @@ +/* + * 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.node; + +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.node.utils.ShorthandParseException; +import me.lucko.luckperms.common.node.utils.ShorthandParser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ShorthandParserTest { + + private static Stream testParse() { + return Stream.of( + // numeric range + Arguments.of("{2-4}", new String[]{"2", "3", "4"}), + Arguments.of("{4-2}", new String[]{"2", "3", "4"}), + Arguments.of("{2147483647-2147483647}", new String[]{"2147483647"}), + + // character range + Arguments.of("{a-d}", new String[]{"a", "b", "c", "d"}), + Arguments.of("{A-D}", new String[]{"A", "B", "C", "D"}), + Arguments.of("{D-A}", new String[]{"A", "B", "C", "D"}), + + // list + Arguments.of("{aa,bb,cc}", new String[]{"aa", "bb", "cc"}), + Arguments.of("{aa|bb|cc}", new String[]{"aa", "bb", "cc"}), + Arguments.of("{aa,bb|cc}", new String[]{"aa", "bb", "cc"}), + + // groups + Arguments.of("he{y|llo} {1-2}", new String[]{"hey 1", "hey 2", "hello 1", "hello 2"}), + Arguments.of("my.permission.{test,hi}", new String[]{"my.permission.test", "my.permission.hi"}), + Arguments.of("my.permission.{a-c}", new String[]{"my.permission.a", "my.permission.b", "my.permission.c"}), + + // groups - using () instead + Arguments.of("he(y|llo) (1-2)", new String[]{"hey 1", "hey 2", "hello 1", "hello 2"}), + Arguments.of("my.permission.(test,hi)", new String[]{"my.permission.test", "my.permission.hi"}), + Arguments.of("my.permission.(a-c)", new String[]{"my.permission.a", "my.permission.b", "my.permission.c"}) + ); + } + + @ParameterizedTest + @MethodSource + public void testParse(String shorthand, String[] expected) throws ShorthandParseException { + Assertions.assertEquals(ImmutableSet.copyOf(expected), ShorthandParser.expandShorthand(shorthand)); + } + + @ParameterizedTest + @ValueSource(strings = { + "{1-1000}", + "{1000-1}", + "{!-က}", + "{က-!}", + "{1-100}{1-100}{1-100}", + "{5--2147483647}" + }) + public void testTooManyElements(String shorthand) { + assertThrows(ShorthandParseException.class, () -> ShorthandParser.expandShorthand(shorthand)); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/query/DataSelectorTest.java b/common/src/test/java/me/lucko/luckperms/common/query/DataSelectorTest.java new file mode 100644 index 000000000..e3e3c7a85 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/query/DataSelectorTest.java @@ -0,0 +1,84 @@ +/* + * 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.query; + +import me.lucko.luckperms.common.model.HolderType; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.query.QueryMode; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.query.dataorder.DataQueryOrder; +import net.luckperms.api.query.dataorder.DataQueryOrderFunction; +import net.luckperms.api.query.dataorder.DataTypeFilter; +import net.luckperms.api.query.dataorder.DataTypeFilterFunction; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class DataSelectorTest { + private static final PermissionHolderIdentifier IDENTIFIER = new PermissionHolderIdentifier(HolderType.USER, "Notch"); + + @Test + public void testDefault() { + DataType[] types = DataSelector.selectOrder(QueryOptionsImpl.DEFAULT_CONTEXTUAL, IDENTIFIER); + assertArrayEquals(new DataType[]{DataType.TRANSIENT, DataType.NORMAL}, types); + } + + @Test + public void testOrdering() { + QueryOptions transientFirst = new QueryOptionsBuilderImpl(QueryMode.CONTEXTUAL) + .option(DataQueryOrderFunction.KEY, DataQueryOrderFunction.always(DataQueryOrder.TRANSIENT_FIRST)) + .build(); + + QueryOptions transientLast = new QueryOptionsBuilderImpl(QueryMode.CONTEXTUAL) + .option(DataQueryOrderFunction.KEY, DataQueryOrderFunction.always(DataQueryOrder.TRANSIENT_LAST)) + .build(); + + DataType[] types = DataSelector.selectOrder(transientFirst, IDENTIFIER); + assertArrayEquals(new DataType[]{DataType.TRANSIENT, DataType.NORMAL}, types); + + types = DataSelector.selectOrder(transientLast, IDENTIFIER); + assertArrayEquals(new DataType[]{DataType.NORMAL, DataType.TRANSIENT}, types); + } + + @Test + public void testSelection() { + QueryOptions normalOnly = new QueryOptionsBuilderImpl(QueryMode.CONTEXTUAL) + .option(DataTypeFilterFunction.KEY, DataTypeFilterFunction.always(DataTypeFilter.NORMAL_ONLY)) + .build(); + + QueryOptions transientOnly = new QueryOptionsBuilderImpl(QueryMode.CONTEXTUAL) + .option(DataTypeFilterFunction.KEY, DataTypeFilterFunction.always(DataTypeFilter.TRANSIENT_ONLY)) + .build(); + + DataType[] types = DataSelector.selectOrder(normalOnly, IDENTIFIER); + assertArrayEquals(new DataType[]{DataType.NORMAL}, types); + + types = DataSelector.selectOrder(transientOnly, IDENTIFIER); + assertArrayEquals(new DataType[]{DataType.TRANSIENT}, types); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/node/utils/ShorthandParserTest.java b/common/src/test/java/me/lucko/luckperms/common/query/QueryOptionsTest.java similarity index 52% rename from common/src/test/java/me/lucko/luckperms/common/node/utils/ShorthandParserTest.java rename to common/src/test/java/me/lucko/luckperms/common/query/QueryOptionsTest.java index 2cdbc5bc2..4e79cd856 100644 --- a/common/src/test/java/me/lucko/luckperms/common/node/utils/ShorthandParserTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/query/QueryOptionsTest.java @@ -23,48 +23,40 @@ * SOFTWARE. */ -package me.lucko.luckperms.common.node.utils; - -import com.google.common.collect.ImmutableSet; +package me.lucko.luckperms.common.query; +import net.luckperms.api.query.Flag; +import net.luckperms.api.query.QueryMode; +import net.luckperms.api.query.QueryOptions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; -public class ShorthandParserTest { - - private static void test(String shorthand, String... expected) { - assertEquals(ImmutableSet.copyOf(expected), ShorthandParser.expandShorthand(shorthand)); - } - - @Test - void testNumericRange() { - test("{2-4}", "2", "3", "4"); - } - - @Test - void testCharacterRange() { - test("{a-d}", "a", "b", "c", "d"); - test("{A-D}", "A", "B", "C", "D"); - } - - @Test - void testList() { - test("{aa,bb,cc}", "aa", "bb", "cc"); - test("{aa|bb|cc}", "aa", "bb", "cc"); - test("{aa,bb|cc}", "aa", "bb", "cc"); - } +public class QueryOptionsTest { @Test - void testGroups() { - test("he{y|llo} {1-2}", "hey 1", "hey 2", "hello 1", "hello 2"); - test("my.permission.{test,hi}", "my.permission.test", "my.permission.hi"); - test("my.permission.{a-c}", "my.permission.a", "my.permission.b", "my.permission.c"); - - // use ( ) instead - test("he(y|llo) (1-2)", "hey 1", "hey 2", "hello 1", "hello 2"); - test("my.permission.(test,hi)", "my.permission.test", "my.permission.hi"); - test("my.permission.(a-c)", "my.permission.a", "my.permission.b", "my.permission.c"); + public void testFlags() { + QueryOptions options = new QueryOptionsBuilderImpl(QueryMode.CONTEXTUAL).build(); + assertSame(QueryOptionsImpl.DEFAULT_CONTEXTUAL, options); + assertEquals(Flag.values().length, options.flags().size()); + + for (Flag flag : Flag.values()) { + assertTrue(options.flag(flag)); + } + + options = new QueryOptionsBuilderImpl(QueryMode.CONTEXTUAL).flag(Flag.APPLY_INHERITANCE_NODES_WITHOUT_WORLD_CONTEXT, false).build(); + assertEquals(Flag.values().length - 1, options.flags().size()); + + for (Flag flag : Flag.values()) { + if (flag == Flag.APPLY_INHERITANCE_NODES_WITHOUT_WORLD_CONTEXT) { + assertFalse(options.flag(flag)); + } else { + assertTrue(options.flag(flag)); + } + } } } diff --git a/common/src/test/java/me/lucko/luckperms/common/sender/AbstractSenderTest.java b/common/src/test/java/me/lucko/luckperms/common/sender/AbstractSenderTest.java new file mode 100644 index 000000000..0c3fda6e8 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/sender/AbstractSenderTest.java @@ -0,0 +1,58 @@ +/* + * 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.sender; + +import com.google.common.collect.ImmutableList; +import me.lucko.luckperms.common.locale.Message; +import net.kyori.adventure.text.Component; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class AbstractSenderTest { + + @Test + public void testSplitNewlines() { + Component component = Message.joinNewline( + Component.text("hello"), + Component.text("world"), + Message.joinNewline( + Component.text("foo"), + Component.text("bar") + ) + ); + + List components = ImmutableList.copyOf(AbstractSender.splitNewlines(component)); + assertEquals(4, components.size()); + assertEquals(Component.text("hello"), components.get(0)); + assertEquals(Component.text("world"), components.get(1)); + assertEquals(Component.text("foo"), components.get(2)); + assertEquals(Component.text("bar"), components.get(3)); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/AbstractStorageTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/AbstractStorageTest.java new file mode 100644 index 000000000..7564a785c --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/AbstractStorageTest.java @@ -0,0 +1,443 @@ +/* + * 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.storage; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.actionlog.LogPage; +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.actionlog.filter.ActionFilters; +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.filter.PageParameters; +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.StorageImplementation; +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; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +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; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.AdditionalAnswers.answer; +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 abstract class AbstractStorageTest { + + @Mock protected LuckPermsPlugin plugin; + @Mock protected LuckPermsBootstrap bootstrap; + @Mock protected LuckPermsConfiguration configuration; + + protected StorageImplementation storage; + + @BeforeEach + public final void setupMocksAndStorage() 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) -> AbstractStorageTest.class.getClassLoader().getResourceAsStream(path))); + lenient().when(this.plugin.getEventDispatcher()).thenReturn(mock(EventDispatcher.class)); + + this.storage = makeStorage(this.plugin); + this.storage.init(); + } + + protected abstract StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception; + + protected void cleanupResources() { + // do nothing + } + + @AfterEach + public final void shutdownStorage() { + this.storage.shutdown(); + cleanupResources(); + } + + @Test + public void testActionLog() throws Exception { + UUID sourceUuid = UUID.randomUUID(); + UUID targetUuid = UUID.randomUUID(); + + Instant baseTime = Instant.now(); + + Function mockAction = i -> LoggedAction.build() + .source(i % 2 == 0 ? sourceUuid : UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.USER) + .target(targetUuid) + .targetName("Test Target") + .description("hello " + i) + .timestamp(baseTime.plusSeconds(i)) + .build(); + + for (int i = 0; i < 100; i++) { + this.storage.logAction(mockAction.apply(-i)); + } + for (int i = 0; i < 100; i++) { + this.storage.logAction(mockAction.apply(i)); + } + for (int i = 100; i < 200; i++) { + this.storage.logAction(mockAction.apply(-i)); + } + + for (int i = 0; i < 10; i++) { + this.storage.logAction(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.GROUP) + .targetName(i % 2 == 0 ? "test_group" : "dummy") + .description("group test " + i) + .timestamp(baseTime) + .build()); + } + + for (int i = 0; i < 10; i++) { + this.storage.logAction(LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test Source") + .targetType(Action.Target.Type.TRACK) + .targetName(i % 2 == 0 ? "test_track" : "dummy") + .description("track test " + i) + .timestamp(baseTime) + .build()); + } + + LogPage page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 1)); + assertEquals(ImmutableList.of( + mockAction.apply(98), + mockAction.apply(96), + mockAction.apply(94), + mockAction.apply(92), + mockAction.apply(90) + ), page.getContent()); + List positions = page.getNumberedContent().stream().map(LogPage.Entry::position).collect(Collectors.toList()); + assertEquals(ImmutableList.of(1, 2, 3, 4, 5), positions); + assertEquals(150, page.getTotalEntries()); + + page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 3)); + assertEquals(ImmutableList.of( + mockAction.apply(78), + mockAction.apply(76), + mockAction.apply(74), + mockAction.apply(72), + mockAction.apply(70) + ), page.getContent()); + positions = page.getNumberedContent().stream().map(LogPage.Entry::position).collect(Collectors.toList()); + assertEquals(ImmutableList.of(11, 12, 13, 14, 15), positions); + assertEquals(150, page.getTotalEntries()); + + page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(5, 31)); + assertEquals(150, page.getTotalEntries()); + assertEquals(0, page.getContent().size()); + + page = this.storage.getLogPage(ActionFilters.source(sourceUuid), new PageParameters(500, 1)); + assertEquals(150, page.getTotalEntries()); + assertEquals(150, page.getContent().size()); + + page = this.storage.getLogPage(ActionFilters.source(sourceUuid), null); + assertEquals(150, page.getTotalEntries()); + assertEquals(150, page.getContent().size()); + + page = this.storage.getLogPage(ActionFilters.all(), null); + assertEquals(320, page.getTotalEntries()); + assertEquals(320, page.getContent().size()); + + page = this.storage.getLogPage(ActionFilters.user(targetUuid), new PageParameters(5, 1)); + assertEquals(300, page.getTotalEntries()); + + page = this.storage.getLogPage(ActionFilters.group("test_group"), new PageParameters(10, 1)); + assertEquals(5, page.getContent().size()); + assertEquals( + ImmutableList.of("group test 8", "group test 6", "group test 4", "group test 2", "group test 0"), + page.getContent().stream().map(LoggedAction::getDescription).collect(Collectors.toList()) + ); + + page = this.storage.getLogPage(ActionFilters.track("test_track"), new PageParameters(10, 1)); + assertEquals(5, page.getContent().size()); + assertEquals( + ImmutableList.of("track test 8", "track test 6", "track test 4", "track test 2", "track test 0"), + page.getContent().stream().map(LoggedAction::getDescription).collect(Collectors.toList()) + ); + + page = this.storage.getLogPage(ActionFilters.search("hello"), new PageParameters(500, 1)); + assertEquals(300, page.getContent().size()); + } + + @Test + public void testSavePlayerData() throws Exception { + UUID uniqueId = UUID.randomUUID(); + + // clean insert + PlayerSaveResult r1 = this.storage.savePlayerData(uniqueId, "Player1"); + assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT), r1.getOutcomes()); + assertNull(r1.getOtherUniqueIds()); + assertNull(r1.getPreviousUsername()); + + // no change expected + PlayerSaveResult r2 = this.storage.savePlayerData(uniqueId, "Player1"); + assertEquals(ImmutableSet.of(Outcome.NO_CHANGE), r2.getOutcomes()); + assertNull(r2.getOtherUniqueIds()); + assertNull(r2.getPreviousUsername()); + + // changed username + PlayerSaveResult r3 = this.storage.savePlayerData(uniqueId, "Player2"); + assertEquals(ImmutableSet.of(Outcome.USERNAME_UPDATED), r3.getOutcomes()); + assertNull(r3.getOtherUniqueIds()); + assertTrue("Player1".equalsIgnoreCase(r3.getPreviousUsername())); + + // changed uuid + UUID newUniqueId = UUID.randomUUID(); + PlayerSaveResult r4 = this.storage.savePlayerData(newUniqueId, "Player2"); + assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT, Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME), r4.getOutcomes()); + assertNotNull(r4.getOtherUniqueIds()); + assertEquals(ImmutableSet.of(uniqueId), r4.getOtherUniqueIds()); + assertNull(r2.getPreviousUsername()); + } + + @Test + public void testGetPlayerUniqueIdAndName() throws Exception { + UUID uniqueId = UUID.randomUUID(); + String username = "Player1"; + + this.storage.savePlayerData(uniqueId, username); + + assertEquals(uniqueId, this.storage.getPlayerUniqueId("Player1")); + assertTrue(username.equalsIgnoreCase(this.storage.getPlayerName(uniqueId))); + } + + @Test + public void testGetPlayerUniqueIdAndNameNull() throws Exception { + assertNull(this.storage.getPlayerUniqueId("Player1")); + assertNull(this.storage.getPlayerName(UUID.randomUUID())); + } + + @Test + public void testSaveAndLoadGroup() throws Exception { + StandardGroupManager groupManager = new StandardGroupManager(this.plugin); + + //noinspection unchecked,rawtypes + lenient().when(this.plugin.getGroupManager()).thenReturn((GroupManager) groupManager); + + Group group = this.storage.createAndLoadGroup("test"); + + Node node1 = Permission.builder() + .permission("test.1") + .withContext("server", "test") + .build(); + Node node2 = Permission.builder() + .permission("test.2") + .withContext("world", "test") + .build(); + Node node3 = Permission.builder() + .permission("test.3") + .expiry(1, TimeUnit.HOURS) + .withContext("server", "test") + .withContext("world", "test") + .withContext("hello", "test") + .build(); + + group.normalData().add(node1); + group.normalData().add(node2); + group.normalData().add(node3); + + Set nodes = group.normalData().asSet(); + assertEquals(3, nodes.size()); + + this.storage.saveGroup(group); + groupManager.unload("test"); + + Group loaded = this.storage.loadGroup("test").orElse(null); + assertNotNull(loaded); + assertNotSame(group, loaded); + assertEquals(nodes, loaded.normalData().asSet()); + + // now edit the loaded group - removing a node and adding a new one - and ensure + // the resulting diff (produced via RecordedNodeMap#exportChanges) is persisted correctly + Node node4 = Permission.builder() + .permission("test.4") + .withContext("server", "test2") + .build(); + + loaded.normalData().remove(node2); + loaded.normalData().add(node4); + + Set editedNodes = loaded.normalData().asSet(); + assertEquals(ImmutableSet.of(node1, node3, node4), editedNodes); + + this.storage.saveGroup(loaded); + groupManager.unload("test"); + + Group reloaded = this.storage.loadGroup("test").orElse(null); + assertNotNull(reloaded); + assertNotSame(loaded, reloaded); + assertEquals(editedNodes, reloaded.normalData().asSet()); + } + + @Test + public void testSaveAndDeleteUser() throws Exception { + 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()); + + // now edit the user - removing a node and adding a new one - and ensure the + // resulting diff (produced via RecordedNodeMap#exportChanges) is persisted correctly + PermissionNode anotherPermission = Permission.builder() + .permission("test.2") + .withContext("world", "test") + .build(); + + user.normalData().remove(examplePermission); + user.normalData().add(anotherPermission); + + Set editedNodes = user.normalData().asSet(); + assertEquals(ImmutableSet.of(defaultGroupNode, anotherPermission), editedNodes); + + this.storage.saveUser(user); + assertTrue(this.storage.getUniqueUsers().contains(exampleUniqueId)); + + user = this.storage.loadUser(exampleUniqueId, exampleUsername); + assertEquals(editedNodes, user.normalData().asSet()); + } + + @Test + public void testBulkLoadUsers() throws Exception { + StandardUserManager userManager = new StandardUserManager(this.plugin); + + //noinspection unchecked,rawtypes + when(this.plugin.getUserManager()).thenReturn((UserManager) userManager); + + Map users = new HashMap<>(); + for (int i = 0; i < 5; i++) { + UUID uuid = UUID.randomUUID(); + String username = "User" + i; + users.put(uuid, username); + + this.storage.savePlayerData(uuid, username); + + User user = this.storage.loadUser(uuid, username); + user.setNode(DataType.NORMAL, Permission.builder() + .permission("test.1") + .withContext("server", "test") + .build(), true); + this.storage.saveUser(user); + } + + userManager.retainAll(List.of()); + + Set usersToLoad = new HashSet<>(users.keySet()); + usersToLoad.add(UUID.randomUUID()); // add a user that doesn't exist + + Map loadedUsers = this.storage.loadUsers(usersToLoad); + assertEquals(usersToLoad.size(), loadedUsers.size()); + + UUID uuid = users.keySet().iterator().next(); + User user = loadedUsers.get(uuid); + assertNotNull(user); + + assertEquals(2, user.normalData().asList().size()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/ConfigurateStorageTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/ConfigurateStorageTest.java new file mode 100644 index 000000000..22d1db444 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/ConfigurateStorageTest.java @@ -0,0 +1,141 @@ +/* + * 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.storage; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.implementation.StorageImplementation; +import me.lucko.luckperms.common.storage.implementation.file.CombinedConfigurateStorage; +import me.lucko.luckperms.common.storage.implementation.file.SeparatedConfigurateStorage; +import me.lucko.luckperms.common.storage.implementation.file.loader.HoconLoader; +import me.lucko.luckperms.common.storage.implementation.file.loader.JsonLoader; +import me.lucko.luckperms.common.storage.implementation.file.loader.TomlLoader; +import me.lucko.luckperms.common.storage.implementation.file.loader.YamlLoader; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; + +import static org.mockito.Mockito.lenient; + +public class ConfigurateStorageTest { + + @Nested + class SeparatedYaml extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new SeparatedConfigurateStorage(plugin, "YAML", new YamlLoader(), ".yml", "yaml-storage"); + } + } + + @Nested + class SeparatedJson extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new SeparatedConfigurateStorage(plugin, "JSON", new JsonLoader(), ".json", "json-storage"); + } + } + + @Nested + class SeparatedHocon extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new SeparatedConfigurateStorage(plugin, "HOCON", new HoconLoader(), ".conf", "hocon-storage"); + } + } + + @Nested + class SeparatedToml extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new SeparatedConfigurateStorage(plugin, "TOML", new TomlLoader(), ".toml", "toml-storage"); + } + } + + @Nested + class CombinedYaml extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new CombinedConfigurateStorage(plugin, "YAML", new YamlLoader(), ".yml", "yaml-storage"); + } + } + + @Nested + class CombinedJson extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new CombinedConfigurateStorage(plugin, "JSON", new JsonLoader(), ".json", "json-storage"); + } + } + + @Nested + class CombinedHocon extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new CombinedConfigurateStorage(plugin, "HOCON", new HoconLoader(), ".conf", "hocon-storage"); + } + } + + @Nested + class CombinedToml extends AbstractStorageTest { + @TempDir + private Path directory; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + lenient().when(this.bootstrap.getDataDirectory()).thenReturn(this.directory); + return new CombinedConfigurateStorage(plugin, "TOML", new TomlLoader(), ".toml", "toml-storage"); + } + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/MongoStorageTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/MongoStorageTest.java new file mode 100644 index 000000000..56e0b90fe --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/MongoStorageTest.java @@ -0,0 +1,140 @@ +/* + * 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.storage; + +import com.mongodb.MongoClient; +import com.mongodb.ServerAddress; +import com.mongodb.client.MongoDatabase; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.implementation.StorageImplementation; +import me.lucko.luckperms.common.storage.implementation.mongodb.MongoStorage; +import me.lucko.luckperms.common.storage.misc.StorageCredentials; +import org.bson.Document; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Tag("docker") +public class MongoStorageTest extends AbstractStorageTest { + + private static final String DATABASE = "minecraft"; + private static final String PREFIX = "luckperms_"; + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("mongo")) + .withExposedPorts(27017); + private String host; + private int port; + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + this.container.start(); + this.host = this.container.getHost(); + this.port = this.container.getFirstMappedPort(); + + StorageCredentials credentials = new StorageCredentials( + this.host + ":" + this.port, + DATABASE, + "", + "" + ); + return new MongoStorage(plugin, credentials, PREFIX, ""); + } + + @Override + protected void cleanupResources() { + this.container.stop(); + } + + @Test + public void testIndexesAreCreatedAndInitIsIdempotent() throws Exception { + this.storage.shutdown(); + this.storage.init(); + + try (MongoClient client = new MongoClient(new ServerAddress(this.host, this.port))) { + MongoDatabase database = client.getDatabase(DATABASE); + + assertIndexCreatedOnce( + database, + PREFIX + "uuid", + "_id_", + new Document("_id", 1) + ); + + assertIndexCreatedOnce( + database, + PREFIX + "uuid", + "name_1", + new Document("name", 1) + ); + + assertIndexCreatedOnce( + database, + PREFIX + "users", + "permissions.key_1", + new Document("permissions.key", 1) + ); + + assertIndexCreatedOnce( + database, + PREFIX + "groups", + "permissions.key_1", + new Document("permissions.key", 1) + ); + } + } + + private static void assertIndexCreatedOnce( + MongoDatabase database, + String collectionName, + String indexName, + Document expectedKey) { + List collectionIndexes = database.getCollection(collectionName).listIndexes().into(new ArrayList<>()); + Document collectionIndex = collectionIndexes.stream() + .filter(index -> indexName.equals(index.getString("name"))) + .findFirst().orElse(null); + + assertNotNull(collectionIndex); + + assertEquals( + expectedKey, + collectionIndex.get("key", Document.class) + ); + + // Ensure index has not been duplicated. + assertEquals( + 1L, + collectionIndexes.stream() + .filter(index -> indexName.equals(index.getString("name"))) + .count() + ); + } +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/RestStorageTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/RestStorageTest.java new file mode 100644 index 000000000..ec81c3c15 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/RestStorageTest.java @@ -0,0 +1,63 @@ +/* + * 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.storage; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.implementation.StorageImplementation; +import me.lucko.luckperms.common.storage.implementation.rest.RestStorage; +import org.junit.jupiter.api.Tag; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; +import org.testcontainers.utility.DockerImageName; + +@Tag("docker") +public class RestStorageTest extends AbstractStorageTest { + + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("ghcr.io/luckperms/rest-api")) + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(RestStorageTest.class))) + .withExposedPorts(8080) + .waitingFor(new WaitAllStrategy() + .withStrategy(Wait.forListeningPort()) + .withStrategy(Wait.forLogMessage(".*Successfully enabled.*", 1)) + ); + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + this.container.start(); + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + return new RestStorage(plugin, "http://" + host + ":" + port + "/", null); + } + + @Override + protected void cleanupResources() { + this.container.stop(); + } +} 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 new file mode 100644 index 000000000..dc0e39f47 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/SqlStorageTest.java @@ -0,0 +1,122 @@ +/* + * 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.storage; + +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.storage.implementation.StorageImplementation; +import me.lucko.luckperms.common.storage.implementation.sql.SqlStorage; +import me.lucko.luckperms.common.storage.implementation.sql.StatementProcessor; +import me.lucko.luckperms.common.storage.implementation.sql.connection.ConnectionFactory; +import me.lucko.luckperms.common.storage.implementation.sql.connection.file.H2ConnectionFactory; +import me.lucko.luckperms.common.storage.implementation.sql.connection.file.NonClosableConnection; +import net.luckperms.api.actionlog.Action; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.time.Instant; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SqlStorageTest extends AbstractStorageTest { + + @Override + protected StorageImplementation makeStorage(LuckPermsPlugin plugin) throws Exception { + return new SqlStorage(plugin, new TestH2ConnectionFactory(), "luckperms_"); + } + + @Test + public void testRecreateTables() throws Exception { + SqlStorage sql = (SqlStorage) this.storage; + + LoggedAction testAction = LoggedAction.build() + .source(UUID.randomUUID()) + .sourceName("Test") + .targetType(Action.Target.Type.TRACK) + .targetName("test") + .description("test ") + .timestamp(Instant.now()) + .build(); + + // perform an action - ensure it works + this.storage.logAction(testAction); + + // delete the table + try (Connection c = sql.getConnectionFactory().getConnection()) { + c.createStatement().execute("DROP TABLE `luckperms_actions`"); + } + + // perform the action again - expect an exception + assertThrows(SQLException.class, () -> this.storage.logAction(testAction)); + + // recreate the table & repeat the action, ensure it works + this.storage.init(); + this.storage.logAction(testAction); + } + + private static class TestH2ConnectionFactory implements ConnectionFactory { + private final NonClosableConnection connection; + + TestH2ConnectionFactory() throws SQLException { + this.connection = new NonClosableConnection( + DriverManager.getConnection("jdbc:h2:mem:test") + ); + } + + @Override + public Connection getConnection() { + return this.connection; + } + + @Override + public String getImplementationName() { + return "H2"; + } + + @Override + public StorageMetadata getMeta() { + return new StorageMetadata(); + } + + @Override + public void init(LuckPermsPlugin plugin) { + + } + + @Override + public StatementProcessor getStatementProcessor() { + return H2ConnectionFactory.STATEMENT_PROCESSOR; + } + + @Override + public void shutdown() throws Exception { + this.connection.shutdown(); + } + } +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorageUnitTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorageUnitTest.java new file mode 100644 index 000000000..08c8451c0 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/implementation/mongodb/MongoStorageUnitTest.java @@ -0,0 +1,69 @@ +/* + * 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.storage.implementation.mongodb; + +import net.luckperms.api.node.Node; +import org.bson.Document; +import org.junit.jupiter.api.Test; + +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 MongoStorageUnitTest { + + @Test + public void testNodeFromDocExpiryLong() { + Document document = new Document() + .append("key", "test") + .append("value", true) + .append("expiry", 1000L); + + Node node = MongoStorage.nodeFromDoc(document); + assertNotNull(node); + assertEquals("test", node.getKey()); + assertTrue(node.getValue()); + assertNotNull(node.getExpiry()); + assertEquals(1000, node.getExpiry().getEpochSecond()); + } + + // https://github.com/LuckPerms/LuckPerms/issues/3846 + @Test + public void testNodeFromDocExpiryInteger() { + Document document = new Document() + .append("key", "test") + .append("value", true) + .append("expiry", 1000); + + Node node = MongoStorage.nodeFromDoc(document); + assertNotNull(node); + assertEquals("test", node.getKey()); + assertTrue(node.getValue()); + assertNotNull(node.getExpiry()); + assertEquals(1000, node.getExpiry().getEpochSecond()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReaderTest.java b/common/src/test/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReaderTest.java new file mode 100644 index 000000000..8b926dd9e --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/storage/implementation/sql/SchemaReaderTest.java @@ -0,0 +1,172 @@ +/* + * 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.storage.implementation.sql; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SchemaReaderTest { + + private static List readStatements(String type) throws IOException { + List statements; + try (InputStream is = SchemaReaderTest.class.getResourceAsStream("/me/lucko/luckperms/schema/" + type + ".sql")) { + if (is == null) { + throw new IOException("Couldn't locate schema file"); + } + + statements = SchemaReader.getStatements(is); + } + return statements; + } + + @Test + public void testReadH2() throws IOException { + assertEquals(ImmutableList.of( + "CREATE TABLE `{prefix}user_permissions` ( `id` INT AUTO_INCREMENT NOT NULL, `uuid` VARCHAR(36) NOT NULL, `permission` VARCHAR(200) NOT NULL, `value` BOOL NOT NULL, `server` VARCHAR(36) NOT NULL, `world` VARCHAR(64) NOT NULL, `expiry` BIGINT NOT NULL, `contexts` VARCHAR(200) NOT NULL, PRIMARY KEY (`id`))", + "CREATE INDEX ON `{prefix}user_permissions` (`uuid`)", + "CREATE TABLE `{prefix}group_permissions` ( `id` INT AUTO_INCREMENT NOT NULL, `name` VARCHAR(36) NOT NULL, `permission` VARCHAR(200) NOT NULL, `value` BOOL NOT NULL, `server` VARCHAR(36) NOT NULL, `world` VARCHAR(64) NOT NULL, `expiry` BIGINT NOT NULL, `contexts` VARCHAR(200) NOT NULL, PRIMARY KEY (`id`))", + "CREATE INDEX ON `{prefix}group_permissions` (`name`)", + "CREATE TABLE `{prefix}players` ( `uuid` VARCHAR(36) NOT NULL, `username` VARCHAR(16) NOT NULL, `primary_group` VARCHAR(36) NOT NULL, PRIMARY KEY (`uuid`))", + "CREATE INDEX ON `{prefix}players` (`username`)", + "CREATE TABLE `{prefix}groups` ( `name` VARCHAR(36) NOT NULL, PRIMARY KEY (`name`))", + "CREATE TABLE `{prefix}actions` ( `id` INT AUTO_INCREMENT NOT NULL, `time` BIGINT NOT NULL, `actor_uuid` VARCHAR(36) NOT NULL, `actor_name` VARCHAR(100) NOT NULL, `type` CHAR(1) NOT NULL, `acted_uuid` VARCHAR(36) NOT NULL, `acted_name` VARCHAR(36) NOT NULL, `action` VARCHAR(300) NOT NULL, PRIMARY KEY (`id`))", + "CREATE TABLE `{prefix}tracks` ( `name` VARCHAR(36) NOT NULL, `groups` TEXT NOT NULL, PRIMARY KEY (`name`))" + ), readStatements("h2")); + } + + @Test + public void testReadSqlite() throws IOException { + assertEquals(ImmutableList.of( + "CREATE TABLE `{prefix}user_permissions` ( `id` INTEGER PRIMARY KEY NOT NULL, `uuid` VARCHAR(36) NOT NULL, `permission` VARCHAR(200) NOT NULL, `value` BOOL NOT NULL, `server` VARCHAR(36) NOT NULL, `world` VARCHAR(64) NOT NULL, `expiry` BIGINT NOT NULL, `contexts` VARCHAR(200) NOT NULL)", + "CREATE INDEX `{prefix}user_permissions_uuid` ON `{prefix}user_permissions` (`uuid`)", + "CREATE TABLE `{prefix}group_permissions` ( `id` INTEGER PRIMARY KEY NOT NULL, `name` VARCHAR(36) NOT NULL, `permission` VARCHAR(200) NOT NULL, `value` BOOL NOT NULL, `server` VARCHAR(36) NOT NULL, `world` VARCHAR(64) NOT NULL, `expiry` BIGINT NOT NULL, `contexts` VARCHAR(200) NOT NULL)", + "CREATE INDEX `{prefix}group_permissions_name` ON `{prefix}group_permissions` (`name`)", + "CREATE TABLE `{prefix}players` ( `uuid` VARCHAR(36) NOT NULL, `username` VARCHAR(16) NOT NULL, `primary_group` VARCHAR(36) NOT NULL, PRIMARY KEY (`uuid`))", + "CREATE INDEX `{prefix}players_username` ON `{prefix}players` (`username`)", + "CREATE TABLE `{prefix}groups` ( `name` VARCHAR(36) NOT NULL, PRIMARY KEY (`name`))", + "CREATE TABLE `{prefix}actions` ( `id` INTEGER PRIMARY KEY NOT NULL, `time` BIGINT NOT NULL, `actor_uuid` VARCHAR(36) NOT NULL, `actor_name` VARCHAR(100) NOT NULL, `type` CHAR(1) NOT NULL, `acted_uuid` VARCHAR(36) NOT NULL, `acted_name` VARCHAR(36) NOT NULL, `action` VARCHAR(300) NOT NULL)", + "CREATE TABLE `{prefix}tracks` ( `name` VARCHAR(36) NOT NULL, `groups` TEXT NOT NULL, PRIMARY KEY (`name`))" + ), readStatements("sqlite")); + } + + @Test + public void testReadMysql() throws IOException { + ImmutableList expected = ImmutableList.of( + "CREATE TABLE `{prefix}user_permissions` ( `id` INT AUTO_INCREMENT NOT NULL, `uuid` VARCHAR(36) NOT NULL, `permission` VARCHAR(200) NOT NULL, `value` BOOL NOT NULL, `server` VARCHAR(36) NOT NULL, `world` VARCHAR(64) NOT NULL, `expiry` BIGINT NOT NULL, `contexts` VARCHAR(200) NOT NULL, PRIMARY KEY (`id`)) DEFAULT CHARSET = utf8mb4", + "CREATE INDEX `{prefix}user_permissions_uuid` ON `{prefix}user_permissions` (`uuid`)", + "CREATE TABLE `{prefix}group_permissions` ( `id` INT AUTO_INCREMENT NOT NULL, `name` VARCHAR(36) NOT NULL, `permission` VARCHAR(200) NOT NULL, `value` BOOL NOT NULL, `server` VARCHAR(36) NOT NULL, `world` VARCHAR(64) NOT NULL, `expiry` BIGINT NOT NULL, `contexts` VARCHAR(200) NOT NULL, PRIMARY KEY (`id`)) DEFAULT CHARSET = utf8mb4", + "CREATE INDEX `{prefix}group_permissions_name` ON `{prefix}group_permissions` (`name`)", + "CREATE TABLE `{prefix}players` ( `uuid` VARCHAR(36) NOT NULL, `username` VARCHAR(16) NOT NULL, `primary_group` VARCHAR(36) NOT NULL, PRIMARY KEY (`uuid`)) DEFAULT CHARSET = utf8mb4", + "CREATE INDEX `{prefix}players_username` ON `{prefix}players` (`username`)", + "CREATE TABLE `{prefix}groups` ( `name` VARCHAR(36) NOT NULL, PRIMARY KEY (`name`)) DEFAULT CHARSET = utf8mb4", + "CREATE TABLE `{prefix}actions` ( `id` INT AUTO_INCREMENT NOT NULL, `time` BIGINT NOT NULL, `actor_uuid` VARCHAR(36) NOT NULL, `actor_name` VARCHAR(100) NOT NULL, `type` CHAR(1) NOT NULL, `acted_uuid` VARCHAR(36) NOT NULL, `acted_name` VARCHAR(36) NOT NULL, `action` VARCHAR(300) NOT NULL, PRIMARY KEY (`id`)) DEFAULT CHARSET = utf8mb4", + "CREATE TABLE `{prefix}tracks` ( `name` VARCHAR(36) NOT NULL, `groups` TEXT NOT NULL, PRIMARY KEY (`name`)) DEFAULT CHARSET = utf8mb4" + ); + assertEquals(expected, readStatements("mysql")); + assertEquals(expected, readStatements("mariadb")); + } + + @Test + public void testReadPostgres() throws IOException { + assertEquals(ImmutableList.of( + "CREATE TABLE \"{prefix}user_permissions\" ( \"id\" SERIAL PRIMARY KEY NOT NULL, \"uuid\" VARCHAR(36) NOT NULL, \"permission\" VARCHAR(200) NOT NULL, \"value\" BOOL NOT NULL, \"server\" VARCHAR(36) NOT NULL, \"world\" VARCHAR(64) NOT NULL, \"expiry\" BIGINT NOT NULL, \"contexts\" VARCHAR(200) NOT NULL)", + "CREATE INDEX \"{prefix}user_permissions_uuid\" ON \"{prefix}user_permissions\" (\"uuid\")", + "CREATE TABLE \"{prefix}group_permissions\" ( \"id\" SERIAL PRIMARY KEY NOT NULL, \"name\" VARCHAR(36) NOT NULL, \"permission\" VARCHAR(200) NOT NULL, \"value\" BOOL NOT NULL, \"server\" VARCHAR(36) NOT NULL, \"world\" VARCHAR(64) NOT NULL, \"expiry\" BIGINT NOT NULL, \"contexts\" VARCHAR(200) NOT NULL)", + "CREATE INDEX \"{prefix}group_permissions_name\" ON \"{prefix}group_permissions\" (\"name\")", + "CREATE TABLE \"{prefix}players\" ( \"uuid\" VARCHAR(36) PRIMARY KEY NOT NULL, \"username\" VARCHAR(16) NOT NULL, \"primary_group\" VARCHAR(36) NOT NULL)", + "CREATE INDEX \"{prefix}players_username\" ON \"{prefix}players\" (\"username\")", + "CREATE TABLE \"{prefix}groups\" ( \"name\" VARCHAR(36) PRIMARY KEY NOT NULL)", + "CREATE TABLE \"{prefix}actions\" ( \"id\" SERIAL PRIMARY KEY NOT NULL, \"time\" BIGINT NOT NULL, \"actor_uuid\" VARCHAR(36) NOT NULL, \"actor_name\" VARCHAR(100) NOT NULL, \"type\" CHAR(1) NOT NULL, \"acted_uuid\" VARCHAR(36) NOT NULL, \"acted_name\" VARCHAR(36) NOT NULL, \"action\" VARCHAR(300) NOT NULL)", + "CREATE TABLE \"{prefix}tracks\" ( \"name\" VARCHAR(36) PRIMARY KEY NOT NULL, \"groups\" TEXT NOT NULL)" + ), readStatements("postgresql")); + } + + @Test + public void testTableFromStatement() throws IOException { + Set allowedTables = ImmutableSet.of( + "luckperms_user_permissions", + "luckperms_group_permissions", + "luckperms_players", + "luckperms_groups", + "luckperms_actions", + "luckperms_tracks" + ); + + for (String type : new String[]{"h2", "mariadb", "mysql", "postgresql", "sqlite"}) { + List tables = readStatements(type).stream() + .map(s -> s.replace("{prefix}", "luckperms_")) + .map(SchemaReader::tableFromStatement) + .collect(Collectors.toList()); + + assertTrue(allowedTables.containsAll(tables)); + } + } + + @Test + public void testFilter() throws IOException { + StatementProcessor processor = s -> s.replace("{prefix}", "luckperms_"); + List statements = readStatements("mysql").stream().map(processor::process).collect(Collectors.toList()); + + // no tables exist, all should be created + List filtered = SchemaReader.filterStatements(statements, ImmutableList.of()); + assertEquals(statements, filtered); + + // all tables exist, none should be created + filtered = SchemaReader.filterStatements(statements, ImmutableList.of( + "luckperms_user_permissions", + "luckperms_group_permissions", + "luckperms_players", + "luckperms_groups", + "luckperms_actions", + "luckperms_tracks" + )); + assertEquals(ImmutableList.of(), filtered); + + // some tables exist, some should be created + filtered = SchemaReader.filterStatements(statements, ImmutableList.of( + "luckperms_user_permissions", + "luckperms_players", + "luckperms_groups", + "luckperms_actions", + "luckperms_tracks" + )); + assertEquals(ImmutableList.of( + "CREATE TABLE `luckperms_group_permissions` ( `id` INT AUTO_INCREMENT NOT NULL, `name` VARCHAR(36) NOT NULL, `permission` VARCHAR(200) NOT NULL, `value` BOOL NOT NULL, `server` VARCHAR(36) NOT NULL, `world` VARCHAR(64) NOT NULL, `expiry` BIGINT NOT NULL, `contexts` VARCHAR(200) NOT NULL, PRIMARY KEY (`id`)) DEFAULT CHARSET = utf8mb4", + "CREATE INDEX `luckperms_group_permissions_name` ON `luckperms_group_permissions` (`name`)" + ), filtered); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/treeview/PermissionRegistryTest.java b/common/src/test/java/me/lucko/luckperms/common/treeview/PermissionRegistryTest.java new file mode 100644 index 000000000..d4c747f6f --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/treeview/PermissionRegistryTest.java @@ -0,0 +1,81 @@ +/* + * 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.treeview; + +import com.google.common.collect.ImmutableSet; +import com.google.gson.JsonObject; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class PermissionRegistryTest { + + @Test + public void testEmpty() { + PermissionRegistry registry = new PermissionRegistry(); + assertEquals(0, registry.rootAsList().size()); + + TreeNode node = registry.getRootNode(); + assertFalse(node.getChildren().isPresent()); + assertEquals(0, node.getChildrenSize()); + } + + @Test + public void testBasic() { + PermissionRegistry registry = new PermissionRegistry(); + registry.insert("minecraft.command.give"); + registry.insert("minecraft.command.time"); + registry.insert("worldedit.clipboard.copy"); + registry.insert("worldedit.clipboard.paste"); + + List permissions = registry.rootAsList(); + assertEquals( + ImmutableSet.of( + "minecraft", "minecraft.command", "minecraft.command.give", "minecraft.command.time", + "worldedit", "worldedit.clipboard", "worldedit.clipboard.copy", "worldedit.clipboard.paste" + ), + ImmutableSet.copyOf(permissions) + ); + } + + @Test + public void testExport() { + PermissionRegistry registry = new PermissionRegistry(); + registry.insert("minecraft.command.give"); + registry.insert("minecraft.command.time"); + registry.insert("worldedit.clipboard.copy"); + registry.insert("worldedit.clipboard.paste"); + + ImmutableTreeNode immutableNode = registry.getRootNode().makeImmutableCopy(); + JsonObject json = immutableNode.toJson(""); + // {"minecraft":{"minecraft.command":{"minecraft.command.give":{},"minecraft.command.time":{}}},"worldedit":{"worldedit.clipboard":{"worldedit.clipboard.copy":{},"worldedit.clipboard.paste":{}}}} + assertEquals("{\"minecraft\":{\"minecraft.command\":{\"minecraft.command.give\":{},\"minecraft.command.time\":{}}},\"worldedit\":{\"worldedit.clipboard\":{\"worldedit.clipboard.copy\":{},\"worldedit.clipboard.paste\":{}}}}", json.toString()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/util/DifferenceTest.java b/common/src/test/java/me/lucko/luckperms/common/util/DifferenceTest.java new file mode 100644 index 000000000..e4f819eaf --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/util/DifferenceTest.java @@ -0,0 +1,84 @@ +/* + * 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.util; + +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.util.Difference.ChangeType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DifferenceTest { + + @Test + public void testSimple() { + Difference diff = new Difference<>(); + assertTrue(diff.isEmpty()); + + diff.recordChange(ChangeType.ADD, "test1"); + diff.recordChange(ChangeType.REMOVE, "test2"); + diff.recordChange(ChangeType.ADD, "test3"); + + assertEquals(ImmutableSet.of("test1", "test3"), diff.getAdded()); + assertEquals(ImmutableSet.of("test2"), diff.getRemoved()); + + assertFalse(diff.isEmpty()); + assertEquals(3, diff.getChanges().size()); + } + + @Test + public void testOverride() { + Difference diff = new Difference<>(); + assertTrue(diff.isEmpty()); + + diff.recordChange(ChangeType.ADD, "test1"); + diff.recordChange(ChangeType.REMOVE, "test1"); + + assertTrue(diff.isEmpty()); + } + + @Test + public void testMerge() { + Difference diff = new Difference<>(); + diff.recordChange(ChangeType.ADD, "test1"); + + Difference diff2 = new Difference<>(); + diff2.recordChange(ChangeType.REMOVE, "test1"); + + assertEquals(1, diff.getChanges().size()); + assertEquals(1, diff2.getChanges().size()); + + Difference returnedDiff = diff.mergeFrom(diff2); + assertSame(diff, returnedDiff); + + assertEquals(0, diff.getChanges().size()); + assertEquals(1, diff2.getChanges().size()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/util/DurationFormatterTest.java b/common/src/test/java/me/lucko/luckperms/common/util/DurationFormatterTest.java new file mode 100644 index 000000000..ce1811315 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/util/DurationFormatterTest.java @@ -0,0 +1,115 @@ +/* + * 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.util; + +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.renderer.TranslatableComponentRenderer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.kyori.adventure.translation.TranslationRegistry; +import net.kyori.adventure.util.UTF8ResourceBundleControl; +import org.junit.jupiter.api.BeforeAll; +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.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DurationFormatterTest { + + private static TranslatableComponentRenderer renderer; + + @BeforeAll + public static void setupRenderer() { + TranslationRegistry registry = TranslationRegistry.create(Key.key("luckperms", "test")); + + ResourceBundle bundle = ResourceBundle.getBundle("luckperms", Locale.ENGLISH, UTF8ResourceBundleControl.get()); + registry.registerAll(Locale.ENGLISH, bundle, false); + + renderer = TranslatableComponentRenderer.usingTranslationSource(registry); + } + + private static String render(Component component) { + return PlainTextComponentSerializer.plainText().serialize(renderer.render(component, Locale.ENGLISH)); + } + + private static Stream testSimple() { + Duration years = ChronoUnit.YEARS.getDuration(); + Duration months = ChronoUnit.MONTHS.getDuration(); + + return Stream.of( + Arguments.of("1 year", years.multipliedBy(1)), + Arguments.of("2 years", years.multipliedBy(2)), + Arguments.of("3 years", years.multipliedBy(3)), + Arguments.of("15 years", years.multipliedBy(15)), + Arguments.of("1 month", months.multipliedBy(1)), + Arguments.of("2 months", months.multipliedBy(2)), + Arguments.of("1 week", Duration.ofDays(7)), + Arguments.of("2 weeks", Duration.ofDays(7 * 2)), + Arguments.of("1 day", Duration.ofDays(1)), + Arguments.of("2 days", Duration.ofDays(2)), + Arguments.of("1 hour", Duration.ofHours(1)), + Arguments.of("2 hours", Duration.ofHours(2)), + Arguments.of("15 hours", Duration.ofHours(15)), + Arguments.of("1 minute", Duration.ofMinutes(1)), + Arguments.of("2 minutes", Duration.ofMinutes(2)), + Arguments.of("15 minutes", Duration.ofMinutes(15)), + Arguments.of("1 second", Duration.ofSeconds(1)), + Arguments.of("2 seconds", Duration.ofSeconds(2)), + Arguments.of("15 seconds", Duration.ofSeconds(15)), + Arguments.of("0 seconds", Duration.ZERO) + ); + } + + @ParameterizedTest + @MethodSource + public void testSimple(String expected, Duration input) { + assertEquals(expected, render(DurationFormatter.LONG.format(input))); + } + + @Test + public void testFormats() { + Duration duration = ChronoUnit.YEARS.getDuration().multipliedBy(5) + .plus(ChronoUnit.MONTHS.getDuration().multipliedBy(4)) + .plus(ChronoUnit.WEEKS.getDuration().multipliedBy(3)) + .plusDays(2) + .plusHours(1) + .plusMinutes(6) + .plusSeconds(7); + + assertEquals("5y 4mo 3w 2d 1h 6m 7s", render(DurationFormatter.CONCISE.format(duration))); + assertEquals("5y 4mo 3w", render(DurationFormatter.CONCISE_LOW_ACCURACY.format(duration))); + assertEquals("5 years 4 months 3 weeks 2 days 1 hour 6 minutes 7 seconds", render(DurationFormatter.LONG.format(duration))); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/util/DurationParserTest.java b/common/src/test/java/me/lucko/luckperms/common/util/DurationParserTest.java index a96d7fd40..5b6e28c15 100644 --- a/common/src/test/java/me/lucko/luckperms/common/util/DurationParserTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/util/DurationParserTest.java @@ -25,82 +25,100 @@ package me.lucko.luckperms.common.util; -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 org.junit.jupiter.params.provider.ValueSource; import java.time.Duration; import java.time.temporal.ChronoUnit; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class DurationParserTest { - private static void test(Duration expected, String input) { - assertEquals(expected, DurationParser.parseDuration(input)); + private static Stream testSimple() { + Duration years = ChronoUnit.YEARS.getDuration(); + Duration months = ChronoUnit.MONTHS.getDuration(); + + return Stream.of( + Arguments.of("2y", years.multipliedBy(2)), + Arguments.of("3year", years.multipliedBy(3)), + Arguments.of("4years", years.multipliedBy(4)), + Arguments.of("2 y", years.multipliedBy(2)), + Arguments.of("3 year", years.multipliedBy(3)), + Arguments.of("4 years", years.multipliedBy(4)), + + Arguments.of("2mo", months.multipliedBy(2)), + Arguments.of("3month", months.multipliedBy(3)), + Arguments.of("4months", months.multipliedBy(4)), + Arguments.of("2 mo", months.multipliedBy(2)), + Arguments.of("3 month", months.multipliedBy(3)), + Arguments.of("4 months", months.multipliedBy(4)), + + Arguments.of("2w", Duration.ofDays(7 * 2)), + Arguments.of("3week", Duration.ofDays(7 * 3)), + Arguments.of("4weeks", Duration.ofDays(7 * 4)), + Arguments.of("2 w", Duration.ofDays(7 * 2)), + Arguments.of("3 week", Duration.ofDays(7 * 3)), + Arguments.of("4 weeks", Duration.ofDays(7 * 4)), + + Arguments.of("2d", Duration.ofDays(2)), + Arguments.of("3day", Duration.ofDays(3)), + Arguments.of("4days", Duration.ofDays(4)), + Arguments.of("2 d", Duration.ofDays(2)), + Arguments.of("3 day", Duration.ofDays(3)), + Arguments.of("4 days", Duration.ofDays(4)), + + Arguments.of("2h", Duration.ofHours(2)), + Arguments.of("3hour", Duration.ofHours(3)), + Arguments.of("4hours", Duration.ofHours(4)), + Arguments.of("2 h", Duration.ofHours(2)), + Arguments.of("3 hour", Duration.ofHours(3)), + Arguments.of("4 hours", Duration.ofHours(4)), + + Arguments.of("2m", Duration.ofMinutes(2)), + Arguments.of("3min", Duration.ofMinutes(3)), + Arguments.of("4mins", Duration.ofMinutes(4)), + Arguments.of("5minute", Duration.ofMinutes(5)), + Arguments.of("6minutes", Duration.ofMinutes(6)), + Arguments.of("2 m", Duration.ofMinutes(2)), + Arguments.of("3 min", Duration.ofMinutes(3)), + Arguments.of("4 mins", Duration.ofMinutes(4)), + Arguments.of("5 minute", Duration.ofMinutes(5)), + Arguments.of("6 minutes", Duration.ofMinutes(6)), + + Arguments.of("2s", Duration.ofSeconds(2)), + Arguments.of("3sec", Duration.ofSeconds(3)), + Arguments.of("4secs", Duration.ofSeconds(4)), + Arguments.of("5second", Duration.ofSeconds(5)), + Arguments.of("6seconds", Duration.ofSeconds(6)), + Arguments.of("2 s", Duration.ofSeconds(2)), + Arguments.of("3 sec", Duration.ofSeconds(3)), + Arguments.of("4 secs", Duration.ofSeconds(4)), + Arguments.of("5 second", Duration.ofSeconds(5)), + Arguments.of("6 seconds", Duration.ofSeconds(6)) + ); } - - @Test - void testSimple() { - test(ChronoUnit.YEARS.getDuration().multipliedBy(2), "2y"); - test(ChronoUnit.YEARS.getDuration().multipliedBy(3), "3year"); - test(ChronoUnit.YEARS.getDuration().multipliedBy(4), "4years"); - test(ChronoUnit.YEARS.getDuration().multipliedBy(2), "2 y"); - test(ChronoUnit.YEARS.getDuration().multipliedBy(3), "3 year"); - test(ChronoUnit.YEARS.getDuration().multipliedBy(4), "4 years"); - - test(ChronoUnit.MONTHS.getDuration().multipliedBy(2), "2mo"); - test(ChronoUnit.MONTHS.getDuration().multipliedBy(3), "3month"); - test(ChronoUnit.MONTHS.getDuration().multipliedBy(4), "4months"); - test(ChronoUnit.MONTHS.getDuration().multipliedBy(2), "2 mo"); - test(ChronoUnit.MONTHS.getDuration().multipliedBy(3), "3 month"); - test(ChronoUnit.MONTHS.getDuration().multipliedBy(4), "4 months"); - - test(Duration.ofDays(7 * 2), "2w"); - test(Duration.ofDays(7 * 3), "3week"); - test(Duration.ofDays(7 * 4), "4weeks"); - test(Duration.ofDays(7 * 2), "2 w"); - test(Duration.ofDays(7 * 3), "3 week"); - test(Duration.ofDays(7 * 4), "4 weeks"); - - test(Duration.ofDays(2), "2d"); - test(Duration.ofDays(3), "3day"); - test(Duration.ofDays(4), "4days"); - test(Duration.ofDays(2), "2 d"); - test(Duration.ofDays(3), "3 day"); - test(Duration.ofDays(4), "4 days"); - - test(Duration.ofHours(2), "2h"); - test(Duration.ofHours(3), "3hour"); - test(Duration.ofHours(4), "4hours"); - test(Duration.ofHours(2), "2 h"); - test(Duration.ofHours(3), "3 hour"); - test(Duration.ofHours(4), "4 hours"); - - test(Duration.ofMinutes(2), "2m"); - test(Duration.ofMinutes(3), "3min"); - test(Duration.ofMinutes(4), "4mins"); - test(Duration.ofMinutes(5), "5minute"); - test(Duration.ofMinutes(6), "6minutes"); - test(Duration.ofMinutes(2), "2 m"); - test(Duration.ofMinutes(3), "3 min"); - test(Duration.ofMinutes(4), "4 mins"); - test(Duration.ofMinutes(5), "5 minute"); - test(Duration.ofMinutes(6), "6 minutes"); - - test(Duration.ofSeconds(2), "2s"); - test(Duration.ofSeconds(3), "3sec"); - test(Duration.ofSeconds(4), "4secs"); - test(Duration.ofSeconds(5), "5second"); - test(Duration.ofSeconds(6), "6seconds"); - test(Duration.ofSeconds(2), "2 s"); - test(Duration.ofSeconds(3), "3 sec"); - test(Duration.ofSeconds(4), "4 secs"); - test(Duration.ofSeconds(5), "5 second"); - test(Duration.ofSeconds(6), "6 seconds"); + + @ParameterizedTest + @MethodSource + public void testSimple(String input, Duration expected) { + assertEquals(expected, DurationParser.parseDuration(input)); } - @Test - void testCombined() { + @ParameterizedTest + @ValueSource(strings = { + "5y 4mo 3w 2d 1h 6m 7s", + "5y4mo3w2d1h6m7s", + "5 years 4 months 3 weeks 2 days 1 hour 6 minutes 7 seconds", + "5y, 4mo, 3w, 2d, 1h, 6m, 7s", + "5y,4mo,3w,2d,1h,6m,7s", + "5 years, 4 months, 3 weeks, 2 days, 1 hour, 6 minutes, 7 seconds" + }) + public void testCombined(String input) { Duration expected = ChronoUnit.YEARS.getDuration().multipliedBy(5) .plus(ChronoUnit.MONTHS.getDuration().multipliedBy(4)) .plus(ChronoUnit.WEEKS.getDuration().multipliedBy(3)) @@ -109,20 +127,17 @@ void testCombined() { .plusMinutes(6) .plusSeconds(7); - test(expected, "5y 4mo 3w 2d 1h 6m 7s"); - test(expected, "5y4mo3w2d1h6m7s"); - test(expected, "5 years 4 months 3 weeks 2 days 1 hour 6 minutes 7 seconds"); - - test(expected, "5y, 4mo, 3w, 2d, 1h, 6m, 7s"); - test(expected, "5y,4mo,3w,2d,1h,6m,7s"); - test(expected, "5 years, 4 months, 3 weeks, 2 days, 1 hour, 6 minutes, 7 seconds"); + assertEquals(expected, DurationParser.parseDuration(input)); } - @Test - void testFail() { - assertThrows(IllegalArgumentException.class, () -> DurationParser.parseDuration("definitely not a duration")); - assertThrows(IllegalArgumentException.class, () -> DurationParser.parseDuration("still 1 not a duration")); - assertThrows(IllegalArgumentException.class, () -> DurationParser.parseDuration("still 1s not a duration")); + @ParameterizedTest + @ValueSource(strings = { + "definitely not a duration", + "still 1 not a duration", + "still 1s not a duration" + }) + public void testFail(String input) { + assertThrows(IllegalArgumentException.class, () -> DurationParser.parseDuration(input)); } } diff --git a/common/src/test/java/me/lucko/luckperms/common/util/EnumNamerTest.java b/common/src/test/java/me/lucko/luckperms/common/util/EnumNamerTest.java index e3c517ce5..5764c48e9 100644 --- a/common/src/test/java/me/lucko/luckperms/common/util/EnumNamerTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/util/EnumNamerTest.java @@ -26,19 +26,20 @@ package me.lucko.luckperms.common.util; import com.google.common.collect.ImmutableMap; - import org.junit.jupiter.api.Test; +import java.util.Locale; + import static org.junit.jupiter.api.Assertions.assertEquals; public class EnumNamerTest { @Test - void testSimple() { + public void testSimple() { EnumNamer namer = new EnumNamer<>( TestEnum.class, ImmutableMap.of(TestEnum.THING, "hi"), - v -> v.name().toLowerCase().replace('_', '-') + v -> v.name().toLowerCase(Locale.ROOT).replace('_', '-') ); assertEquals("test", namer.name(TestEnum.TEST)); diff --git a/common/src/test/java/me/lucko/luckperms/common/util/IteratorsTest.java b/common/src/test/java/me/lucko/luckperms/common/util/IteratorsTest.java index af30c6375..df70b349c 100644 --- a/common/src/test/java/me/lucko/luckperms/common/util/IteratorsTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/util/IteratorsTest.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.common.util; import com.google.common.collect.ImmutableList; - import org.junit.jupiter.api.Test; import java.util.List; @@ -36,12 +35,12 @@ public class IteratorsTest { @Test - void testDivideEmpty() { + public void testDivideEmpty() { assertEquals(ImmutableList.of(), Iterators.divideIterable(ImmutableList.of(), 2)); } @Test - void testDivideSimple() { + public void testDivideSimple() { List> expected = ImmutableList.of( ImmutableList.of("one", "two"), ImmutableList.of("three", "four"), @@ -57,7 +56,7 @@ void testDivideSimple() { } @Test - void testDivideBoundary() { + public void testDivideBoundary() { List> expected = ImmutableList.of( ImmutableList.of("one", "two"), ImmutableList.of("three", "four") diff --git a/common/src/test/java/me/lucko/luckperms/common/util/PaginatedTest.java b/common/src/test/java/me/lucko/luckperms/common/util/PaginatedTest.java deleted file mode 100644 index 8865db349..000000000 --- a/common/src/test/java/me/lucko/luckperms/common/util/PaginatedTest.java +++ /dev/null @@ -1,70 +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.common.util; - -import com.google.common.collect.ImmutableList; - -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class PaginatedTest { - - @Test - void testSimple() { - Paginated paginated = new Paginated<>(ImmutableList.of("one", "two", "three", "four", "five")); - assertEquals(3, paginated.getMaxPages(2)); - assertEquals(1, paginated.getMaxPages(5)); - assertEquals(1, paginated.getMaxPages(6)); - - List> page1 = paginated.getPage(1, 2); - assertEquals(2, page1.size()); - assertEquals("one", page1.get(0).value()); - assertEquals(1, page1.get(0).position()); - assertEquals("two", page1.get(1).value()); - assertEquals(2, page1.get(1).position()); - - List> page2 = paginated.getPage(2, 2); - assertEquals(2, page2.size()); - assertEquals("three", page2.get(0).value()); - assertEquals(3, page2.get(0).position()); - assertEquals("four", page2.get(1).value()); - assertEquals(4, page2.get(1).position()); - - List> page3 = paginated.getPage(3, 2); - assertEquals(1, page3.size()); - assertEquals("five", page3.get(0).value()); - assertEquals(5, page3.get(0).position()); - - assertThrows(IllegalStateException.class, () -> paginated.getPage(4, 2)); - assertThrows(IllegalArgumentException.class, () -> paginated.getPage(0, 2)); - assertThrows(IllegalArgumentException.class, () -> paginated.getPage(-1, 2)); - } - -} diff --git a/common/src/test/java/me/lucko/luckperms/common/util/UniqueIdTypeTest.java b/common/src/test/java/me/lucko/luckperms/common/util/UniqueIdTypeTest.java new file mode 100644 index 000000000..e20dae47c --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/util/UniqueIdTypeTest.java @@ -0,0 +1,69 @@ +/* + * 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.util; + +import me.lucko.luckperms.common.event.EventDispatcher; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.AdditionalAnswers.returnsSecondArg; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class UniqueIdTypeTest { + + @Mock private LuckPermsPlugin plugin; + @Mock private EventDispatcher dispatcher; + + @BeforeEach + public void setupMocks() { + when(this.plugin.getEventDispatcher()).thenReturn(this.dispatcher); + when(this.dispatcher.dispatchUniqueIdDetermineType(any(), anyString())).then(returnsSecondArg()); + } + + @ParameterizedTest + @CsvSource({ + "797a99ba-c040-4f04-8cfc-6b01a4890d2f, authenticated", + "5d41402a-bc4b-3a76-b971-9d911017c592, unauthenticated", + "cfa4fcb1-a786-23fc-b956-33914c7bb373, npc", + "00000000-0000-0000-0000-000000000000, unknown" + }) + public void testParse(UUID uuid, String expectedType) { + UniqueIdType uniqueIdType = UniqueIdType.determineType(uuid, this.plugin); + assertEquals(expectedType, uniqueIdType.getType()); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/verbose/BooleanExpressionTest.java b/common/src/test/java/me/lucko/luckperms/common/verbose/BooleanExpressionTest.java index 904914c6e..c151fe853 100644 --- a/common/src/test/java/me/lucko/luckperms/common/verbose/BooleanExpressionTest.java +++ b/common/src/test/java/me/lucko/luckperms/common/verbose/BooleanExpressionTest.java @@ -26,28 +26,34 @@ package me.lucko.luckperms.common.verbose; import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler; +import me.lucko.luckperms.common.verbose.expression.BooleanExpressionCompiler.VariableEvaluator; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; public class BooleanExpressionTest { - private static void test(String expression, boolean expected) { - assertEquals( - expected, - BooleanExpressionCompiler.compile(expression).eval(var -> var.equals("true")), - expression + " is not " + expected - ); + private static final VariableEvaluator STRING_EVAL = var -> var.equals("true"); + + @ParameterizedTest + @ValueSource(strings = { + "false & false | true", + "true | false & false", + "(true & ((true | false) & !(true & false)))" + }) + public void testEvaluatesTrue(String expression) { + assertTrue(BooleanExpressionCompiler.compile(expression).eval(STRING_EVAL)); } - @Test - void testBrackets() { - test("false & false | true", true); - test("false & (false | true)", false); - test("true | false & false", true); - test("(true | false) & false", false); - test("(true & ((true | false) & !(true & false)))", true); + @ParameterizedTest + @ValueSource(strings = { + "false & (false | true)", + "(true | false) & false" + }) + public void testEvaluatesFalse(String expression) { + assertFalse(BooleanExpressionCompiler.compile(expression).eval(STRING_EVAL)); } } diff --git a/common/src/test/java/me/lucko/luckperms/common/verbose/VerboseFilterTest.java b/common/src/test/java/me/lucko/luckperms/common/verbose/VerboseFilterTest.java new file mode 100644 index 000000000..bed0a6180 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/verbose/VerboseFilterTest.java @@ -0,0 +1,137 @@ +/* + * 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.verbose; + +import me.lucko.luckperms.common.cacheddata.result.StringResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; +import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; +import net.luckperms.api.util.Tristate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class VerboseFilterTest { + + @Test + public void testAcceptAll() { + VerboseFilter filter = VerboseFilter.acceptAll(); + assertTrue(filter.isBlank()); + } + + @ParameterizedTest + @CsvSource({ + "luckperms, true", + "luckperms.user, true", + "luckperms.group, false", + "Player1, true", + "Player2, false", + "luckperms & Player1, true", + "permission & luckperms & Player1, true", + "permission & luckperms & Player1 & true, true", + "luckperms & Player2, false", + "luckperms | test, true" + }) + public void testPermissionEvent(String expression, boolean expected) throws InvalidFilterException { + VerboseFilter filter = VerboseFilter.compile(expression); + assertFalse(filter.isBlank()); + + PermissionCheckEvent relevantEvent = new PermissionCheckEvent( + CheckOrigin.INTERNAL, + VerboseCheckTarget.of(VerboseCheckTarget.USER_TYPE, "Player1"), + QueryOptionsImpl.DEFAULT_CONTEXTUAL, + System.currentTimeMillis(), + new Throwable(), + "test", + "luckperms.user.parent.info", + TristateResult.forMonitoredResult(Tristate.TRUE) + ); + + PermissionCheckEvent nonRelevantEvent = new PermissionCheckEvent( + CheckOrigin.INTERNAL, + VerboseCheckTarget.of(VerboseCheckTarget.USER_TYPE, "aaaaaaa"), + QueryOptionsImpl.DEFAULT_CONTEXTUAL, + System.currentTimeMillis(), + new Throwable(), + "test", + "aaaaaaaaa", + TristateResult.forMonitoredResult(Tristate.FALSE) + ); + + assertEquals(expected, filter.evaluate(relevantEvent)); + assertFalse(filter.evaluate(nonRelevantEvent)); + } + + @ParameterizedTest + @CsvSource({ + "nametags, true", + "nametags.nametag, true", + "nametags.other, false", + "Player1, true", + "Player2, false", + "nametags & Player1, true", + "meta & nametags & Player1, true", + "meta & nametags & Player1 & admin, true", + "nametags & Player2, false", + "nametags | test, true" + }) + public void testMetaEvent(String expression, boolean expected) throws InvalidFilterException { + VerboseFilter filter = VerboseFilter.compile(expression); + assertFalse(filter.isBlank()); + + MetaCheckEvent relevantEvent = new MetaCheckEvent( + CheckOrigin.INTERNAL, + VerboseCheckTarget.of(VerboseCheckTarget.USER_TYPE, "Player1"), + QueryOptionsImpl.DEFAULT_CONTEXTUAL, + System.currentTimeMillis(), + new Throwable(), + "test", + "nametags.nametag", + StringResult.of("ADMIN") + ); + + MetaCheckEvent nonRelevantEvent = new MetaCheckEvent( + CheckOrigin.INTERNAL, + VerboseCheckTarget.of(VerboseCheckTarget.USER_TYPE, "aaaaaaa"), + QueryOptionsImpl.DEFAULT_CONTEXTUAL, + System.currentTimeMillis(), + new Throwable(), + "test", + "aaaaaaaaa", + StringResult.of("aaaaaa") + ); + + assertEquals(expected, filter.evaluate(relevantEvent)); + assertFalse(filter.evaluate(nonRelevantEvent)); + } + +} diff --git a/common/src/test/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithmTest.java b/common/src/test/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithmTest.java new file mode 100644 index 000000000..b784999b9 --- /dev/null +++ b/common/src/test/java/me/lucko/luckperms/common/webeditor/socket/SignatureAlgorithmTest.java @@ -0,0 +1,73 @@ +/* + * 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.webeditor.socket; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.security.KeyPair; +import java.security.PublicKey; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SignatureAlgorithmTest { + + @ParameterizedTest + @EnumSource + public void testKeypairGenerate(SignatureAlgorithm algorithm) { + algorithm.generateKeyPair(); + } + + @ParameterizedTest + @EnumSource + public void testSignVerify(SignatureAlgorithm algorithm) { + KeyPair keyPair = algorithm.generateKeyPair(); + + String signature = algorithm.sign(keyPair.getPrivate(), "test"); + assertTrue(algorithm.verify(keyPair.getPublic(), "test", signature)); + + assertFalse(algorithm.verify(keyPair.getPublic(), "test", "bleh")); + assertFalse(algorithm.verify(keyPair.getPublic(), "test", "")); + assertFalse(algorithm.verify(keyPair.getPublic(), "test", null)); + } + + @Test + public void testParseAndVerifyRSA() { + // the base64 values are generated from javascript crypto.subtle + PublicKey publicKey = SignatureAlgorithm.V1_RSA.parsePublicKey("MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+Fhv9SmQIENpseq81/BCmGJ8Pf94X4yNdsrNaZ0uNGasUlIM/+aRrSA8X586BEGc7qZeVidegW4yM3LufaDnkoAyIQij7IfHzO09H3VdbLWF+RQY/dWj/cd6O2QkrjMXsW+LKkeKAsY2KTzxoyR9vLcTAP229mQPxiFWWidMAVeU3EzHWtV74UAuycG1Ja3kRtS031lugJJKAlgXUVlAF8tI+aoTQljpndptrhRBPtnUxtCCxj8jFM5houD5010zXIAzsAjg2NPHl/R/qypfHFMWYlcCGIbKMN61gM6lRyglLC+2dxSBw7b+GHTGHwoK3UMhqyonlRAP4W+UpA/tT/LazXHXalYOz8IYcnQgb4Np7pw2TFY2HA5sR4ZfCTnE1bemlWMHbjBc5CAnb7KyZVpFsxPLvcuSLaM4t3CyXBSwDJTMNj9aLSYg6FNAwnEcskRdgkrf23+1E30CaOsIKv4Um7SlnB+6qnxmRWpcs4rWPuS7IJXemaYks+gkgZr+Wt6ITPx8NRbyO1eLwsOOyN6g6DcZwc/2MTl1ItbP0+jAvE2NIU1KU0+uuyobZh1cldDGfaboshh9Ni9D4SSWzugPN9Ohs0QueEo3qi1Z6Jv9Jx2Bx1QlIV7FNEGpU6kDknRejewm+qzl5m0fxnfNH46x2FneUisqTea9Vo9suN8CAwEAAQ=="); + assertTrue(SignatureAlgorithm.V1_RSA.verify(publicKey, "hello world", "Z9XU+AIcHF7Y96grX/NLuNN2fI3nmuXfFss1QbTg80j3Jh8jZRMyFRfWz7rc1OToEsrAQXFY426nxN7JdXTTSvw4kErIn6amvTJBqEqWB+rA1FKJqnsXbl3gIG8UqwqBfMlYh5tYhBddsKVc3jW+4kPPGBgUfxgneHcpocgrwi3aX1vqvxJ4y49M1hs0hFH1VnO1VXcffQWnZRnEuUccYH61DHZHiFyWfo2SF6wdNMJG51idUBgZY7zyMnLRzL+07N9MrDJHkc9J4O5HRDuvVefoRNcvW/tpeVDMsLynP3psmyt33euds6LkdVtExolngepKAuGE9JBtnjFEWFakQ+INhvHZ7P4jGiLKRf7kDdckLqJxsH25w6MYzsq4jHTVbrzKehUAx0nnWhL3QrSLTwvly0WHTd4yd/rTVM2JUb+z5FPzuVQP6VgQmrwXYAhA6swkE/1poBWOgsCIe7rvHn3PYvU1D66fXe4lHbyQMAmAu39GLu3RpnmeXuiUT/yygMqvb1Rr8hTeOkjxQOuus+70ybkC21nVCigRFpI4ktvILe09F8jkL6VFHYtz6fKqXKJBTT2gIKijFCJeqCgkCxNnoLeOU+hsS3pZQUwuZS7l0Eyax1eKyelOv6zg10j2ido7/55dE3U0OZGOQ6VWRq8NiPuURId5NzFGURkDda8=")); + } + + @Test + public void testParseAndVerifyECDSA() { + // the base64 values are generated from javascript crypto.subtle + PublicKey publicKey = SignatureAlgorithm.V2_ECDSA.parsePublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkF5EWzdsbmVOYprtfMleBZYASm7AXBQQCE29xR2hpGkjVi4Fra/KPazRShqyGvQXY24sINsxIPEd4XamDfFAaQ=="); + assertTrue(SignatureAlgorithm.V2_ECDSA.verify(publicKey, "hello world", "XAZJMxOlR5Mcq7nJxU4oS1fYyViYH1FZxWOXwOC+LRXYF8KeP58k5KLTjc35L974t3RukwAqflul0HY64bJT3w==")); + } + +} diff --git a/common/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/common/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/common/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/fabric/build.gradle b/fabric/build.gradle index da2275c27..4a318031e 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -1,66 +1,76 @@ -import net.fabricmc.loom.task.RemapJarTask - plugins { - id 'com.github.johnrengelman.shadow' version '4.0.1' - id 'fabric-loom' version '0.6-SNAPSHOT' + alias(libs.plugins.shadow) + alias(libs.plugins.loom) +} + +base { + archivesName = 'luckperms' } -archivesBaseName = 'luckperms' +tasks.withType(JavaCompile).configureEach { + options.release = 25 +} repositories { maven { url 'https://maven.fabricmc.net/' } - mavenLocal() + maven { + url 'https://maven.nucleoid.xyz/' + content { + includeGroup('eu.pb4') + } + } } -def minecraftVersion = '1.16.4' -def yarnBuild = 7 -def loaderVersion = '0.10.8' -def fabricApiVersion = '0.28.4+1.16' +configurations { + shade + implementation.extendsFrom shade + + jarInJar + implementation.extendsFrom jarInJar +} dependencies { - // Fabric Stuff, We don't specifically target only a single version but yarn mappings require a version to be specified. - minecraft "com.mojang:minecraft:${minecraftVersion}" - mappings "net.fabricmc:yarn:${minecraftVersion}+build.${yarnBuild}:v2" - modImplementation "net.fabricmc:fabric-loader:${loaderVersion}" + // https://modmuss50.me/fabric.html + minecraft 'com.mojang:minecraft:26.2' + implementation 'net.fabricmc:fabric-loader:0.18.4' Set apiModules = [ 'fabric-api-base', - 'fabric-command-api-v1', + 'fabric-command-api-v2', 'fabric-lifecycle-events-v1', - 'fabric-networking-api-v1' + 'fabric-networking-api-v1', + 'fabric-entity-events-v1', + 'fabric-permission-api-v1', ] apiModules.forEach { - modImplementation(fabricApi.module(it, fabricApiVersion)) + implementation(fabricApi.module(it, '0.152.1+26.2')) } - include(modImplementation('me.lucko:fabric-permissions-api:0.1-SNAPSHOT')) + jarInJar 'me.lucko:fabric-permissions-api:0.7.0' + implementation 'eu.pb4:placeholder-api:3.0.0+26.1' - compile project(':common') + shade project(':common') + shade project(':common:minecraft') + shade project(':common:placeholders') } +loom.nestJars(tasks.named("shadowJar"), configurations.jarInJar) + processResources { inputs.property 'version', project.ext.fullVersion - - from(sourceSets.main.resources.srcDirs) { - include 'fabric.mod.json' + filesMatching('**/fabric.mod.json') { expand 'version': project.ext.fullVersion } - - from(sourceSets.main.resources.srcDirs) { - exclude 'fabric.mod.json' - } } shadowJar { - archiveName = "luckpermsfabric-${project.ext.fullVersion}-dev.jar" + archiveFileName = "LuckPerms-Fabric-${project.ext.fullVersion}.jar" + configurations = [project.configurations.shade] dependencies { - exclude('net.fabricmc:.*') include(dependency('net.luckperms:.*')) include(dependency('me.lucko.luckperms:.*')) - // We don't want to include the mappings in the jar do we? - exclude '/mappings/*' } relocate 'net.kyori.adventure', 'me.lucko.luckperms.lib.adventure' @@ -77,21 +87,13 @@ shadowJar { relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' + relocate 'org.yaml.snakeyaml', 'me.lucko.luckperms.lib.yaml' } -task remappedShadowJar(type: RemapJarTask) { - dependsOn tasks.shadowJar - input = tasks.shadowJar.archivePath - addNestedDependencies = true - archiveName = "LuckPerms-Fabric-${project.ext.fullVersion}.jar" -} - -tasks.assemble.dependsOn tasks.remappedShadowJar - artifacts { - archives remappedShadowJar - shadow shadowJar + archives shadowJar } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricClassPathAppender.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricClassPathAppender.java index 88403a80d..665d1ec97 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricClassPathAppender.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricClassPathAppender.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.fabric; import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; - import net.fabricmc.loader.launch.common.FabricLauncherBase; import java.net.MalformedURLException; diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java index 8086bc3f6..1111a7f28 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCommandExecutor.java @@ -25,94 +25,15 @@ package me.lucko.luckperms.fabric; -import com.mojang.brigadier.Command; -import com.mojang.brigadier.context.CommandContext; -import com.mojang.brigadier.suggestion.SuggestionProvider; -import com.mojang.brigadier.suggestion.Suggestions; -import com.mojang.brigadier.suggestion.SuggestionsBuilder; -import com.mojang.brigadier.tree.ArgumentCommandNode; -import com.mojang.brigadier.tree.LiteralCommandNode; - -import me.lucko.luckperms.common.command.CommandManager; -import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; -import me.lucko.luckperms.common.sender.Sender; - -import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; -import net.minecraft.server.command.ServerCommandSource; - -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; -import static net.minecraft.server.command.CommandManager.argument; -import static net.minecraft.server.command.CommandManager.literal; - -public class FabricCommandExecutor extends CommandManager implements Command, SuggestionProvider { - private static final String[] COMMAND_ALIASES = new String[] {"luckperms", "lp", "perm", "perms", "permission", "permissions"}; - - private final LPFabricPlugin plugin; +import me.lucko.luckperms.common.minecraft.command.MinecraftCommandExecutor; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +public class FabricCommandExecutor extends MinecraftCommandExecutor { public FabricCommandExecutor(LPFabricPlugin plugin) { super(plugin); - this.plugin = plugin; } public void register() { - CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> { - for (String alias : COMMAND_ALIASES) { - LiteralCommandNode cmd = literal(alias) - .executes(this) - .build(); - - ArgumentCommandNode args = argument("args", greedyString()) - .suggests(this) - .executes(this) - .build(); - - cmd.addChild(args); - dispatcher.getRoot().addChild(cmd); - } - }); + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> register(dispatcher)); } - - @Override - public int run(CommandContext ctx) { - Sender wrapped = this.plugin.getSenderFactory().wrap(ctx.getSource()); - - int start = ctx.getRange().getStart(); - List arguments = ArgumentTokenizer.EXECUTE.tokenizeInput(ctx.getInput().substring(start)); - - String label = arguments.remove(0); - if (label.startsWith("/")) { - label = label.substring(1); - } - - executeCommand(wrapped, label, arguments); - return Command.SINGLE_SUCCESS; - } - - @Override - public CompletableFuture getSuggestions(CommandContext ctx, SuggestionsBuilder builder) { - Sender wrapped = this.plugin.getSenderFactory().wrap(ctx.getSource()); - - int idx = builder.getStart(); - - String buffer = ctx.getInput().substring(idx); - idx += buffer.length(); - - List arguments = ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(buffer); - if (!arguments.isEmpty()) { - idx -= arguments.get(arguments.size() - 1).length(); - } - - List completions = tabCompleteCommand(wrapped, arguments); - - // Offset the builder from the current string range so suggestions are placed in the right spot - builder = builder.createOffset(idx); - for (String completion : completions) { - builder.suggest(completion); - } - return builder.buildFuture(); - } - } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricConfigAdapter.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricConfigAdapter.java index fbc07eb2c..a2fec3ef3 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricConfigAdapter.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricConfigAdapter.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.config.generic.adapter.ConfigurateConfigAdapter; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.hocon.HoconConfigurationLoader; import ninja.leaping.configurate.loader.ConfigurationLoader; diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricEventBus.java b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricEventBus.java index 59a7d41b6..0799cb5ce 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricEventBus.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricEventBus.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.event.AbstractEventBus; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import net.fabricmc.loader.api.ModContainer; public class FabricEventBus extends AbstractEventBus { 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 5b5127d67..6a9284d08 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSenderFactory.java @@ -26,90 +26,28 @@ package me.lucko.luckperms.fabric; import me.lucko.fabric.api.permissions.v0.Permissions; -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.model.MixinUser; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import me.lucko.luckperms.common.minecraft.MinecraftSenderFactory; +import me.lucko.luckperms.fabric.mixin.CommandSourceStackAccessor; import net.luckperms.api.util.Tristate; -import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.text.Text; - -import java.util.Locale; -import java.util.UUID; - -public class FabricSenderFactory extends SenderFactory { - private final LPFabricPlugin plugin; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +public class FabricSenderFactory extends MinecraftSenderFactory { public FabricSenderFactory(LPFabricPlugin plugin) { super(plugin); - this.plugin = plugin; - } - - @Override - protected LPFabricPlugin getPlugin() { - return this.plugin; - } - - @Override - protected UUID getUniqueId(ServerCommandSource commandSource) { - if (commandSource.getEntity() != null) { - return commandSource.getEntity().getUuid(); - } - return Sender.CONSOLE_UUID; - } - - @Override - protected String getName(ServerCommandSource commandSource) { - String name = commandSource.getName(); - if (commandSource.getEntity() != null && name.equals("Server")) { - return Sender.CONSOLE_NAME; - } - return name; } @Override - protected void sendMessage(ServerCommandSource sender, Component message) { - Locale locale = null; - if (sender.getEntity() instanceof ServerPlayerEntity) { - locale = ((MixinUser) sender.getEntity()).getCachedLocale(); - } - sender.sendFeedback(toNativeText(TranslationManager.render(message, locale)), false); + protected CommandSource getSource(CommandSourceStack sender) { + return ((CommandSourceStackAccessor) sender).getSource(); } @Override - protected Tristate getPermissionValue(ServerCommandSource commandSource, String node) { - switch (Permissions.getPermissionValue(commandSource, node)) { - case TRUE: - return Tristate.TRUE; - case FALSE: - return Tristate.FALSE; - case DEFAULT: - return Tristate.UNDEFINED; - default: - throw new AssertionError(); - } - } - - @Override - protected boolean hasPermission(ServerCommandSource commandSource, String node) { - return getPermissionValue(commandSource, node).asBoolean(); - } - - @Override - protected void performCommand(ServerCommandSource sender, String command) { - sender.getMinecraftServer().getCommandManager().execute(sender, command); - } - - @Override - protected boolean isConsole(ServerCommandSource sender) { - return sender.getEntity() == null; - } - - public static Text toNativeText(Component component) { - return Text.Serializer.fromJson(GsonComponentSerializer.gson().serialize(component)); + protected Tristate getPermissionValue(CommandSourceStack commandSource, String node) { + return switch (Permissions.getPermissionValue(commandSource, node)) { + case TRUE -> Tristate.TRUE; + case FALSE -> Tristate.FALSE; + case DEFAULT -> Tristate.UNDEFINED; + }; } } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricBootstrap.java b/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricBootstrap.java index 215b37381..5e4bb3f2a 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricBootstrap.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricBootstrap.java @@ -25,22 +25,18 @@ package me.lucko.luckperms.fabric; -import com.mojang.authlib.GameProfile; - +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsBootstrap; +import me.lucko.luckperms.common.minecraft.MinecraftSchedulerAdapter; import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; import me.lucko.luckperms.common.plugin.logging.Log4jPluginLogger; import me.lucko.luckperms.common.plugin.logging.PluginLogger; -import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; - import net.fabricmc.api.DedicatedServerModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; import net.luckperms.api.platform.Platform; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.network.ServerPlayerEntity; - import org.apache.logging.log4j.LogManager; import java.io.IOException; @@ -48,22 +44,20 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.CountDownLatch; /** * Bootstrap plugin for LuckPerms running on Fabric. */ -public final class LPFabricBootstrap implements LuckPermsBootstrap, DedicatedServerModInitializer { +public final class LPFabricBootstrap extends MinecraftLuckPermsBootstrap implements LuckPermsBootstrap, DedicatedServerModInitializer { private static final String MODID = "luckperms"; - private static final ModContainer MOD_CONTAINER = FabricLoader.getInstance().getModContainer(MODID) - .orElseThrow(() -> new RuntimeException("Could not get the LuckPerms mod container.")); + + /** + * The mod container + */ + private final ModContainer modContainer; /** * The plugin logger @@ -73,7 +67,7 @@ public final class LPFabricBootstrap implements LuckPermsBootstrap, DedicatedSer /** * A scheduler adapter for the platform */ - private final SchedulerAdapter schedulerAdapter; + private final MinecraftSchedulerAdapter schedulerAdapter; /** * The plugin class path appender @@ -100,8 +94,10 @@ public final class LPFabricBootstrap implements LuckPermsBootstrap, DedicatedSer private MinecraftServer server; public LPFabricBootstrap() { + this.modContainer = FabricLoader.getInstance().getModContainer(MODID) + .orElseThrow(() -> new RuntimeException("Could not get the LuckPerms mod container.")); this.logger = new Log4jPluginLogger(LogManager.getLogger(MODID)); - this.schedulerAdapter = new FabricSchedulerAdapter(this); + this.schedulerAdapter = new MinecraftSchedulerAdapter(this); this.classPathAppender = new FabricClassPathAppender(); this.plugin = new LPFabricPlugin(this); } @@ -114,7 +110,7 @@ public PluginLogger getPluginLogger() { } @Override - public SchedulerAdapter getScheduler() { + public MinecraftSchedulerAdapter getScheduler() { return this.schedulerAdapter; } @@ -137,6 +133,7 @@ public void onInitializeServer() { // Register the Server startup/shutdown events now ServerLifecycleEvents.SERVER_STARTING.register(this::onServerStarting); ServerLifecycleEvents.SERVER_STOPPING.register(this::onServerStopping); + ServerLifecycleEvents.SERVER_STOPPED.register(this::onServerStopped); this.plugin.registerFabricListeners(); } @@ -151,6 +148,15 @@ private void onServerStopping(MinecraftServer server) { this.server = null; } + private void onServerStopped(MinecraftServer server) { + if (this.server == null) { + return; + } + + this.plugin.disable(); + this.server = null; + } + @Override public CountDownLatch getLoadLatch() { return this.loadLatch; @@ -161,8 +167,7 @@ public CountDownLatch getEnableLatch() { return this.enableLatch; } - // MinecraftServer singleton getter - + @Override public Optional getServer() { return Optional.ofNullable(this.server); } @@ -171,7 +176,7 @@ public Optional getServer() { @Override public String getVersion() { - return MOD_CONTAINER.getMetadata().getVersion().getFriendlyString(); + return this.modContainer.getMetadata().getVersion().getFriendlyString(); } @Override @@ -201,7 +206,7 @@ public String getServerVersion() { .map(c -> c.getMetadata().getVersion().getFriendlyString()) .orElse("unknown"); - return getServer().map(MinecraftServer::getVersion).orElse("null") + " - fabric-api@" + fabricApiVersion; + return getServer().map(MinecraftServer::getServerVersion).orElse("null") + " - fabric-api@" + fabricApiVersion; } @Override @@ -217,64 +222,12 @@ public Path getConfigDirectory() { @Override public InputStream getResourceStream(String path) { try { - return Files.newInputStream(LPFabricBootstrap.MOD_CONTAINER.getPath(path)); + return Files.newInputStream(this.modContainer.getPath(path)); } catch (IOException e) { return null; } } - @Override - public Optional getPlayer(UUID uniqueId) { - return getServer().map(MinecraftServer::getPlayerManager).map(s -> s.getPlayer(uniqueId)); - } - - @Override - public Optional lookupUniqueId(String username) { - return getServer().map(MinecraftServer::getUserCache).map(c -> c.findByName(username)).map(GameProfile::getId); - - } - - @Override - public Optional lookupUsername(UUID uniqueId) { - return getServer().map(MinecraftServer::getUserCache).map(c -> c.getByUuid(uniqueId)).map(GameProfile::getName); - } - - @Override - public int getPlayerCount() { - return getServer().map(MinecraftServer::getCurrentPlayerCount).orElse(0); - } - @Override - public Collection getPlayerList() { - return getServer().map(MinecraftServer::getPlayerManager) - .map(server -> { - List players = server.getPlayerList(); - List list = new ArrayList<>(players.size()); - for (ServerPlayerEntity player : players) { - list.add(player.getGameProfile().getName()); - } - return list; - }) - .orElse(Collections.emptyList()); - } - - @Override - public Collection getOnlinePlayers() { - return getServer().map(MinecraftServer::getPlayerManager) - .map(server -> { - List players = server.getPlayerList(); - List list = new ArrayList<>(players.size()); - for (ServerPlayerEntity player : players) { - list.add(player.getGameProfile().getId()); - } - return list; - }) - .orElse(Collections.emptyList()); - } - - @Override - public boolean isPlayerOnline(UUID uniqueId) { - return getServer().map(MinecraftServer::getPlayerManager).map(s -> s.getPlayer(uniqueId) != null).orElse(false); - } } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricPlugin.java b/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricPlugin.java index 718ea2b2c..7c184d74b 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricPlugin.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/LPFabricPlugin.java @@ -26,58 +26,40 @@ package me.lucko.luckperms.fabric; import me.lucko.luckperms.common.api.LuckPermsApiProvider; -import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; import me.lucko.luckperms.common.dependencies.Dependency; import me.lucko.luckperms.common.event.AbstractEventBus; -import me.lucko.luckperms.common.locale.TranslationManager; import me.lucko.luckperms.common.messaging.MessagingFactory; -import me.lucko.luckperms.common.model.User; -import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; -import me.lucko.luckperms.common.model.manager.track.StandardTrackManager; -import me.lucko.luckperms.common.model.manager.user.StandardUserManager; -import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; -import me.lucko.luckperms.common.sender.DummyConsoleSender; -import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.tasks.CacheHousekeepingTask; -import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsPlugin; +import me.lucko.luckperms.common.minecraft.listeners.MinecraftAutoOpListener; +import me.lucko.luckperms.common.minecraft.listeners.MinecraftCommandListUpdater; import me.lucko.luckperms.fabric.context.FabricContextManager; import me.lucko.luckperms.fabric.context.FabricPlayerCalculator; import me.lucko.luckperms.fabric.listeners.FabricConnectionListener; -import me.lucko.luckperms.fabric.listeners.PermissionCheckListener; +import me.lucko.luckperms.fabric.listeners.FabricOtherListeners; +import me.lucko.luckperms.fabric.listeners.FabricPermissionsApiV0Listener; +import me.lucko.luckperms.fabric.listeners.FabricPermissionsApiV1Listener; +import me.lucko.luckperms.fabric.listeners.FabricPermissionsListener; import me.lucko.luckperms.fabric.messaging.FabricMessagingFactory; - +import me.lucko.luckperms.fabric.placeholder.FabricPlaceholderApiIntegration; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer; import net.luckperms.api.LuckPerms; -import net.luckperms.api.query.QueryOptions; -import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.ServerOpList; -import java.util.Optional; +import java.io.IOException; import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; - -public class LPFabricPlugin extends AbstractLuckPermsPlugin { - private final LPFabricBootstrap bootstrap; +public class LPFabricPlugin extends MinecraftLuckPermsPlugin { private FabricConnectionListener connectionListener; private FabricCommandExecutor commandManager; private FabricSenderFactory senderFactory; private FabricContextManager contextManager; - private StandardUserManager userManager; - private StandardGroupManager groupManager; - private StandardTrackManager trackManager; public LPFabricPlugin(LPFabricBootstrap bootstrap) { - this.bootstrap = bootstrap; - } - - @Override - public LPFabricBootstrap getBootstrap() { - return this.bootstrap; + super(bootstrap); } protected void registerFabricListeners() { @@ -85,11 +67,17 @@ protected void registerFabricListeners() { this.connectionListener = new FabricConnectionListener(this); this.connectionListener.registerListeners(); - new PermissionCheckListener(this).registerListeners(); + new FabricPermissionsApiV0Listener(this).registerListeners(); + new FabricPermissionsApiV1Listener(this).registerListeners(); + new FabricPermissionsListener().registerListeners(); // Command registration also need to occur early, and will persist across game states as well. - this.commandManager = new FabricCommandExecutor(this); - this.commandManager.register(); + if (!skipCommandRegistration()) { + this.commandManager = new FabricCommandExecutor(this); + this.commandManager.register(); + } + + new FabricOtherListeners(this).registerListeners(); } @Override @@ -126,18 +114,6 @@ protected void registerCommands() { // Too late for Fabric, registered in #registerFabricListeners } - @Override - protected void setupManagers() { - this.userManager = new StandardUserManager(this); - this.groupManager = new StandardGroupManager(this); - this.trackManager = new StandardTrackManager(this); - } - - @Override - protected CalculatorFactory provideCalculatorFactory() { - return new FabricCalculatorFactory(this); - } - @Override protected void setupContextManager() { this.contextManager = new FabricContextManager(this); @@ -160,14 +136,39 @@ protected AbstractEventBus provideEventBus(LuckPermsApiProvider pr protected void registerApiOnPlatform(LuckPerms api) { } - @Override - protected void registerHousekeepingTasks() { - this.bootstrap.getScheduler().asyncRepeating(new ExpireTemporaryTask(this), 3, TimeUnit.SECONDS); - this.bootstrap.getScheduler().asyncRepeating(new CacheHousekeepingTask(this), 2, TimeUnit.MINUTES); - } - @Override protected void performFinalSetup() { + // remove all operators on startup if they're disabled + if (!getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + ServerOpList opList = server.getPlayerList().getOps(); + opList.getEntries().clear(); + try { + opList.save(); + } catch (IOException exception) { + exception.printStackTrace(); + } + }); + } + + // register autoop listener + if (getConfiguration().get(ConfigKeys.AUTO_OP)) { + getApiProvider().getEventBus().subscribe(new MinecraftAutoOpListener(this)); + } + + // register fabric command list updater + if (getConfiguration().get(ConfigKeys.UPDATE_CLIENT_COMMAND_LIST)) { + getApiProvider().getEventBus().subscribe(new MinecraftCommandListUpdater(this)); + } + + // hook with placeholder api, if present + if (FabricLoader.getInstance().isModLoaded("placeholder-api")) { + try { + new FabricPlaceholderApiIntegration(this).register(); + } catch (LinkageError e) { + // ignore + } + } } public FabricSenderFactory getSenderFactory() { @@ -189,44 +190,4 @@ public FabricContextManager getContextManager() { return this.contextManager; } - @Override - public StandardUserManager getUserManager() { - return this.userManager; - } - - @Override - public StandardGroupManager getGroupManager() { - return this.groupManager; - } - - @Override - public StandardTrackManager getTrackManager() { - return this.trackManager; - } - - @Override - public Optional getQueryOptionsForUser(User user) { - return this.bootstrap.getPlayer(user.getUniqueId()).map(player -> this.contextManager.getQueryOptions(player)); - } - - @Override - public Stream getOnlineSenders() { - return Stream.concat( - Stream.of(getConsoleSender()), - this.bootstrap.getServer().map(MinecraftServer::getPlayerManager).map(s -> s.getPlayerList().stream().map(p -> this.senderFactory.wrap(p.getCommandSource()))).orElseGet(Stream::empty) - ); - } - - @Override - public Sender getConsoleSender() { - return this.bootstrap.getServer() - .map(s -> this.senderFactory.wrap(s.getCommandSource())) - .orElseGet(() -> new DummyConsoleSender(this) { - @Override - public void sendMessage(Component message) { - LPFabricPlugin.this.bootstrap.getPluginLogger().info(PlainComponentSerializer.plain().serialize(TranslationManager.render(message))); - } - }); - } - } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricContextManager.java b/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricContextManager.java index eac3eddfc..9b2c527de 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricContextManager.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/context/FabricContextManager.java @@ -25,57 +25,39 @@ package me.lucko.luckperms.fabric.context; -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.ContextManager; -import me.lucko.luckperms.common.context.QueryOptionsCache; +import me.lucko.luckperms.common.context.manager.DetachedContextManager; +import me.lucko.luckperms.common.context.manager.QueryOptionsSupplier; +import me.lucko.luckperms.common.minecraft.context.MinecraftContextManager; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.fabric.model.MixinUser; - -import net.luckperms.api.context.ImmutableContextSet; -import net.luckperms.api.query.OptionKey; import net.luckperms.api.query.QueryOptions; -import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.level.ServerPlayer; +import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; import java.util.UUID; -public class FabricContextManager extends ContextManager { - public static final OptionKey INTEGRATED_SERVER_OWNER = OptionKey.of("integrated_server_owner", Boolean.class); - +public class FabricContextManager extends DetachedContextManager implements MinecraftContextManager { public FabricContextManager(LuckPermsPlugin plugin) { - super(plugin, ServerPlayerEntity.class, ServerPlayerEntity.class); - } - - @Override - public UUID getUniqueId(ServerPlayerEntity player) { - return player.getUuid(); - } - - public QueryOptionsCache newQueryOptionsCache(ServerPlayerEntity player) { - return new QueryOptionsCache<>(player, this); + super(plugin, ServerPlayer.class, ServerPlayer.class); } @Override - public QueryOptionsCache getCacheFor(ServerPlayerEntity subject) { - if (subject == null) { - throw new NullPointerException("subject"); - } - - return ((MixinUser) subject).getQueryOptionsCache(this); + public UUID getUniqueId(ServerPlayer player) { + return player.getUUID(); } @Override - public void invalidateCache(ServerPlayerEntity subject) { - getCacheFor(subject).invalidate(); + public @Nullable QueryOptionsSupplier getQueryOptionsSupplier(ServerPlayer subject) { + Objects.requireNonNull(subject, "subject"); + return ((MixinUser) subject).luckperms$getQueryOptionsCache(this); } @Override - public QueryOptions formQueryOptions(ServerPlayerEntity subject, ImmutableContextSet contextSet) { - QueryOptions.Builder queryOptions = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder(); - if (subject.getServer().isHost(subject.getGameProfile())) { - queryOptions.option(INTEGRATED_SERVER_OWNER, true); + public void customizeQueryOptions(ServerPlayer subject, QueryOptions.Builder builder) { + if (subject.level().getServer().isSingleplayerOwner(subject.nameAndId())) { + builder.option(INTEGRATED_SERVER_OWNER, true); } - - return queryOptions.context(contextSet).build(); } } 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 f3a0c917f..45ccaea38 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 @@ -25,104 +25,26 @@ package me.lucko.luckperms.fabric.context; -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; -import me.lucko.luckperms.common.util.EnumNamer; +import me.lucko.luckperms.common.minecraft.context.MinecraftPlayerCalculator; import me.lucko.luckperms.fabric.LPFabricPlugin; -import me.lucko.luckperms.fabric.event.PlayerChangeWorldCallback; +import net.fabricmc.fabric.api.entity.event.v1.ServerEntityLevelChangeEvents; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; -import net.luckperms.api.context.Context; -import net.luckperms.api.context.ContextCalculator; -import net.luckperms.api.context.ContextConsumer; -import net.luckperms.api.context.ContextSet; -import net.luckperms.api.context.DefaultContextKeys; -import net.luckperms.api.context.ImmutableContextSet; -import net.minecraft.server.MinecraftServer; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.server.world.ServerWorld; -import net.minecraft.util.Identifier; -import net.minecraft.world.GameMode; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.jetbrains.annotations.NotNull; - -import java.util.Optional; import java.util.Set; -public class FabricPlayerCalculator implements ContextCalculator { - private static final EnumNamer GAMEMODE_NAMER = new EnumNamer<>( - GameMode.class, - EnumNamer.LOWER_CASE_NAME - ); - - private final LPFabricPlugin plugin; - - private final boolean gamemode; - private final boolean world; - //private final boolean dimensionType; - +public class FabricPlayerCalculator extends MinecraftPlayerCalculator { public FabricPlayerCalculator(LPFabricPlugin plugin, Set disabled) { - this.plugin = plugin; - this.gamemode = !disabled.contains(DefaultContextKeys.GAMEMODE_KEY); - this.world = !disabled.contains(DefaultContextKeys.WORLD_KEY); - //this.dimensionType = !disabled.contains(DefaultContextKeys.DIMENSION_TYPE_KEY); + super(plugin, disabled); } public void registerListeners() { - PlayerChangeWorldCallback.EVENT.register(this::onWorldChange); - } - - @Override - public void calculate(@NonNull ServerPlayerEntity target, @NonNull ContextConsumer consumer) { - GameMode mode = target.interactionManager.getGameMode(); - if (this.gamemode && mode != null && mode != GameMode.NOT_SET) { - consumer.accept(DefaultContextKeys.GAMEMODE_KEY, GAMEMODE_NAMER.name(mode)); - } - - // TODO: figure out dimension type context too - ServerWorld world = target.getServerWorld(); - if (this.world) { - this.plugin.getConfiguration().get(ConfigKeys.WORLD_REWRITES).rewriteAndSubmit(getContextKey(world.getRegistryKey().getValue()), consumer); - } - } - - @Override - public @NotNull @NonNull ContextSet estimatePotentialContexts() { - ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); - - if (this.gamemode) { - for (GameMode mode : GameMode.values()) { - builder.add(DefaultContextKeys.GAMEMODE_KEY, GAMEMODE_NAMER.name(mode)); - } - } - - // TODO: dimension type - - Optional server = this.plugin.getBootstrap().getServer(); - if (this.world && server.isPresent()) { - Iterable worlds = server.get().getWorlds(); - for (ServerWorld world : worlds) { - String worldName = getContextKey(world.getRegistryKey().getValue()); - if (Context.isValidValue(worldName)) { - builder.add(DefaultContextKeys.WORLD_KEY, worldName); - } - } - } - - return builder.build(); + ServerEntityLevelChangeEvents.AFTER_PLAYER_CHANGE_LEVEL.register(this::onWorldChange); } - private static String getContextKey(Identifier key) { - if (key.getNamespace().equals("minecraft")) { - return key.getPath(); + private void onWorldChange(ServerPlayer player, ServerLevel origin, ServerLevel destination) { + if (this.world || this.dimensionType) { + this.plugin.getContextManager().signalContextUpdate(player); } - return key.toString(); } - - private void onWorldChange(ServerWorld origin, ServerWorld destination, ServerPlayerEntity player) { - if (this.world) { - this.plugin.getContextManager().invalidateCache(player); - } - } - } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/event/PlayerChangeWorldCallback.java b/fabric/src/main/java/me/lucko/luckperms/fabric/event/PreExecuteCommandCallback.java similarity index 70% rename from fabric/src/main/java/me/lucko/luckperms/fabric/event/PlayerChangeWorldCallback.java rename to fabric/src/main/java/me/lucko/luckperms/fabric/event/PreExecuteCommandCallback.java index 2d919ea1b..b478ba74b 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/event/PlayerChangeWorldCallback.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/event/PreExecuteCommandCallback.java @@ -27,16 +27,17 @@ import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.server.world.ServerWorld; +import net.minecraft.commands.CommandSourceStack; -// TODO: Use Fabric API alternative when merged. -public interface PlayerChangeWorldCallback { - Event EVENT = EventFactory.createArrayBacked(PlayerChangeWorldCallback.class, callbacks -> (originalWorld, destination, player) -> { - for (PlayerChangeWorldCallback callback : callbacks) { - callback.onChangeWorld(originalWorld, destination, player); +public interface PreExecuteCommandCallback { + Event EVENT = EventFactory.createArrayBacked(PreExecuteCommandCallback.class, listeners -> (source, input) -> { + for (PreExecuteCommandCallback listener : listeners) { + if (!listener.onPreExecuteCommand(source, input)) { + return false; + } } + return true; }); - void onChangeWorld(ServerWorld originalWorld, ServerWorld destination, ServerPlayerEntity player); + boolean onPreExecuteCommand(CommandSourceStack source, String input); } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/event/SetupPlayerPermissionsEvent.java b/fabric/src/main/java/me/lucko/luckperms/fabric/event/SetupPlayerPermissionsEvent.java new file mode 100644 index 000000000..753825809 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/event/SetupPlayerPermissionsEvent.java @@ -0,0 +1,43 @@ +/* + * 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.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.PermissionSet; + +public interface SetupPlayerPermissionsEvent { + Event EVENT = EventFactory.createArrayBacked(SetupPlayerPermissionsEvent.class, listeners -> (entity, defaults) -> { + PermissionSet set = defaults; + for (SetupPlayerPermissionsEvent listener : listeners) { + set = listener.onSetupPlayerPermissions(entity, set); + } + return set; + }); + + PermissionSet onSetupPlayerPermissions(ServerPlayer entity, PermissionSet defaults); +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java index c93ece113..08acdd0bf 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricConnectionListener.java @@ -26,27 +26,24 @@ package me.lucko.luckperms.fabric.listeners; import com.mojang.authlib.GameProfile; - import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.minecraft.MinecraftSenderFactory; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; -import me.lucko.luckperms.fabric.FabricSenderFactory; import me.lucko.luckperms.fabric.LPFabricPlugin; -import me.lucko.luckperms.fabric.mixin.ServerLoginNetworkHandlerAccessor; +import me.lucko.luckperms.fabric.mixin.ServerLoginPacketListenerImplAccessor; import me.lucko.luckperms.fabric.model.MixinUser; - import net.fabricmc.fabric.api.networking.v1.PacketSender; import net.fabricmc.fabric.api.networking.v1.ServerLoginConnectionEvents; import net.fabricmc.fabric.api.networking.v1.ServerLoginNetworking.LoginSynchronizer; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.kyori.adventure.text.Component; -import net.minecraft.entity.player.PlayerEntity; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.network.ServerLoginNetworkHandler; -import net.minecraft.server.network.ServerPlayNetworkHandler; -import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -65,13 +62,13 @@ public void registerListeners() { ServerPlayConnectionEvents.DISCONNECT.register(this::onDisconnect); } - private void onPreLogin(ServerLoginNetworkHandler netHandler, MinecraftServer server, PacketSender packetSender, LoginSynchronizer sync) { + private void onPreLogin(ServerLoginPacketListenerImpl netHandler, MinecraftServer server, PacketSender packetSender, LoginSynchronizer sync) { /* Called when the player first attempts a connection with the server. */ // Get their profile from the net handler - it should have been initialised by now. - GameProfile profile = ((ServerLoginNetworkHandlerAccessor) netHandler).getGameProfile(); - UUID uniqueId = PlayerEntity.getUuidFromProfile(profile); - String username = profile.getName(); + GameProfile profile = ((ServerLoginPacketListenerImplAccessor) netHandler).getAuthenticatedProfile(); + UUID uniqueId = profile.id(); + String username = profile.name(); if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { this.plugin.getLogger().info("Processing pre-login (sync phase) for " + uniqueId + " - " + username); @@ -81,7 +78,7 @@ private void onPreLogin(ServerLoginNetworkHandler netHandler, MinecraftServer se sync.waitFor(CompletableFuture.runAsync(() -> onPreLoginAsync(netHandler, uniqueId, username), this.plugin.getBootstrap().getScheduler().async())); } - private void onPreLoginAsync(ServerLoginNetworkHandler netHandler, UUID uniqueId, String username) { + private void onPreLoginAsync(ServerLoginPacketListenerImpl netHandler, UUID uniqueId, String username) { if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { this.plugin.getLogger().info("Processing pre-login (async phase) for " + uniqueId + " - " + username); } @@ -104,37 +101,39 @@ private void onPreLoginAsync(ServerLoginNetworkHandler netHandler, UUID uniqueId // deny the connection Component reason = TranslationManager.render(Message.LOADING_DATABASE_ERROR.build()); - netHandler.disconnect(FabricSenderFactory.toNativeText(reason)); + netHandler.disconnect(MinecraftSenderFactory.toNativeText(reason)); this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(uniqueId, username, null); } } - private void onLogin(ServerPlayNetworkHandler netHandler, PacketSender packetSender, MinecraftServer server) { - final ServerPlayerEntity player = netHandler.player; + private void onLogin(ServerGamePacketListenerImpl netHandler, PacketSender packetSender, MinecraftServer server) { + final ServerPlayer player = netHandler.player; if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { - this.plugin.getLogger().info("Processing login for " + player.getUuid() + " - " + player.getGameProfile().getName()); + this.plugin.getLogger().info("Processing login for " + player.getUUID() + " - " + player.getGameProfile().name()); } - final User user = this.plugin.getUserManager().getIfLoaded(player.getUuid()); + final User user = this.plugin.getUserManager().getIfLoaded(player.getUUID()); /* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */ if (user == null) { - this.plugin.getLogger().warn("User " + player.getUuid() + " - " + player.getGameProfile().getName() + + this.plugin.getLogger().warn("User " + player.getUUID() + " - " + player.getGameProfile().id() + " doesn't currently have data pre-loaded - denying login."); Component reason = TranslationManager.render(Message.LOADING_STATE_ERROR.build()); - netHandler.disconnect(FabricSenderFactory.toNativeText(reason)); + netHandler.disconnect(MinecraftSenderFactory.toNativeText(reason)); return; } // init permissions handler - ((MixinUser) player).initializePermissions(user); + ((MixinUser) player).luckperms$initializePermissions(user); this.plugin.getContextManager().signalContextUpdate(player); } - private void onDisconnect(ServerPlayNetworkHandler netHandler, MinecraftServer server) { - handleDisconnect(netHandler.player.getUuid()); + private void onDisconnect(ServerGamePacketListenerImpl netHandler, MinecraftServer server) { + if (!server.isStopped()) { + handleDisconnect(netHandler.player.getUUID()); + } } } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricOtherListeners.java b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricOtherListeners.java new file mode 100644 index 000000000..75b9b8cd6 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricOtherListeners.java @@ -0,0 +1,65 @@ +/* + * 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.listeners; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.fabric.LPFabricPlugin; +import me.lucko.luckperms.fabric.event.PreExecuteCommandCallback; +import net.minecraft.commands.CommandSourceStack; + +import java.util.regex.Pattern; + +public class FabricOtherListeners { + private static final Pattern OP_COMMAND_PATTERN = Pattern.compile("^/?(deop|op)( .*)?$"); + + private final LPFabricPlugin plugin; + + public FabricOtherListeners(LPFabricPlugin plugin) { + this.plugin = plugin; + } + + public void registerListeners() { + PreExecuteCommandCallback.EVENT.register(this::onPreExecuteCommand); + } + + private boolean onPreExecuteCommand(CommandSourceStack source, String input) { + if (input.isEmpty()) { + return true; + } + + if (this.plugin.getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + return true; + } + + if (OP_COMMAND_PATTERN.matcher(input).matches()) { + Message.OP_DISABLED.send(this.plugin.getSenderFactory().wrap(source)); + return false; + } + + return true; + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricPermissionsApiV0Listener.java b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricPermissionsApiV0Listener.java new file mode 100644 index 000000000..a76f48186 --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricPermissionsApiV0Listener.java @@ -0,0 +1,157 @@ +/* + * 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.listeners; + +import me.lucko.fabric.api.permissions.v0.OfflineOptionRequestEvent; +import me.lucko.fabric.api.permissions.v0.OfflinePermissionCheckEvent; +import me.lucko.fabric.api.permissions.v0.OptionRequestEvent; +import me.lucko.fabric.api.permissions.v0.PermissionCheckEvent; +import me.lucko.luckperms.common.cacheddata.result.StringResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.type.MonitoredMetaCache; +import me.lucko.luckperms.common.cacheddata.type.PermissionCache; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import me.lucko.luckperms.common.verbose.VerboseCheckTarget; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.fabric.LPFabricPlugin; +import me.lucko.luckperms.fabric.model.MixinUser; +import net.fabricmc.fabric.api.util.TriState; +import net.luckperms.api.util.Tristate; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Listener to route permission checks made via fabric-permissions-api to LuckPerms. + */ +public class FabricPermissionsApiV0Listener { + private final LPFabricPlugin plugin; + + public FabricPermissionsApiV0Listener(LPFabricPlugin plugin) { + this.plugin = plugin; + } + + public void registerListeners() { + PermissionCheckEvent.EVENT.register(this::onPermissionCheck); + OptionRequestEvent.EVENT.register(this::onOptionRequest); + OfflinePermissionCheckEvent.EVENT.register(this::onOfflinePermissionCheck); + OfflineOptionRequestEvent.EVENT.register(this::onOfflineOptionRequest); + } + + private @NonNull TriState onPermissionCheck(SharedSuggestionProvider source, String permission) { + if (source instanceof CommandSourceStack) { + Entity entity = ((CommandSourceStack) source).getEntity(); + if (entity instanceof ServerPlayer) { + return playerPermissionCheck((ServerPlayer) entity, permission); + } + } + return otherPermissionCheck(source, permission); + } + + private @NonNull Optional onOptionRequest(SharedSuggestionProvider source, String key) { + if (source instanceof CommandSourceStack) { + Entity entity = ((CommandSourceStack) source).getEntity(); + if (entity instanceof ServerPlayer) { + return playerGetOption((ServerPlayer) entity, key); + } + } + return otherGetOption(source, key); + } + + private @NonNull CompletableFuture onOfflinePermissionCheck(UUID uuid, String permission) { + return lookupUser(uuid).thenApplyAsync(user -> { + PermissionCache permissionData = user.getCachedData().getPermissionData(); + return fabricTristate(permissionData.checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result()); + }); + } + + private @NonNull CompletableFuture> onOfflineOptionRequest(UUID uuid, String key) { + return lookupUser(uuid).thenApplyAsync(user -> { + MonitoredMetaCache metaData = user.getCachedData().getMetaData(); + return Optional.ofNullable(metaData.getMetaOrChatMetaValue(key, CheckOrigin.PLATFORM_API)); + }); + } + + public CompletableFuture lookupUser(UUID uuid) { + User user = this.plugin.getUserManager().getIfLoaded(uuid); + if (user != null) { + return CompletableFuture.completedFuture(user); + } + return this.plugin.getStorage().loadUser(uuid, null); + } + + private TriState playerPermissionCheck(ServerPlayer player, String permission) { + return fabricTristate(((MixinUser) player).luckperms$hasPermission(permission)); + } + + private TriState otherPermissionCheck(SharedSuggestionProvider source, String permission) { + if (source instanceof CommandSourceStack) { + String name = ((CommandSourceStack) source).getTextName(); + VerboseCheckTarget target = VerboseCheckTarget.internal(name); + + this.plugin.getVerboseHandler().offerPermissionCheckEvent(CheckOrigin.PLATFORM_API_HAS_PERMISSION, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.UNDEFINED); + this.plugin.getPermissionRegistry().offer(permission); + } + + return TriState.DEFAULT; + } + + private Optional playerGetOption(ServerPlayer player, String key) { + return Optional.ofNullable(((MixinUser) player).luckperms$getOption(key)); + } + + private Optional otherGetOption(SharedSuggestionProvider source, String key) { + if (source instanceof CommandSourceStack) { + String name = ((CommandSourceStack) source).getTextName(); + VerboseCheckTarget target = VerboseCheckTarget.internal(name); + + this.plugin.getVerboseHandler().offerMetaCheckEvent(CheckOrigin.PLATFORM_API, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, key, StringResult.nullResult()); + } + + return Optional.empty(); + } + + private static TriState fabricTristate(Tristate tristate) { + switch (tristate) { + case TRUE: + return TriState.TRUE; + case FALSE: + return TriState.FALSE; + case UNDEFINED: + return TriState.DEFAULT; + default: + throw new AssertionError(); + } + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricPermissionsApiV1Listener.java b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricPermissionsApiV1Listener.java new file mode 100644 index 000000000..21b01fe0a --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricPermissionsApiV1Listener.java @@ -0,0 +1,143 @@ +/* + * 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.listeners; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.fabric.LPFabricPlugin; +import me.lucko.luckperms.fabric.model.MixinUser; +import net.fabricmc.fabric.api.permission.v1.MutablePermissionContext; +import net.fabricmc.fabric.api.permission.v1.PermissionContext; +import net.fabricmc.fabric.api.permission.v1.PermissionEvents; +import net.fabricmc.fabric.api.permission.v1.PermissionNode; +import net.fabricmc.fabric.impl.permission.PermissionContextKey; +import net.luckperms.api.util.Tristate; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.Entity; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** + * Listener to route permission checks made via fabric-permission-api v1 to LuckPerms. + */ +@SuppressWarnings("UnstableApiUsage") +public class FabricPermissionsApiV1Listener { + private static final PermissionContext.Key USER_KEY = new PermissionContextKey<>(Identifier.fromNamespaceAndPath("luckperms", "user")); + + private final LPFabricPlugin plugin; + + public FabricPermissionsApiV1Listener(LPFabricPlugin plugin) { + this.plugin = plugin; + } + + public void registerListeners() { + PermissionEvents.ON_REQUEST.register(this::onPermissionRequest); + PermissionEvents.PREPARE_OFFLINE_PLAYER.register(this::onPrepareOfflinePlayer); + } + + private T onPermissionRequest(PermissionContext ctx, PermissionNode node) { + Codec codec = node.codec(); + + if (codec == Codec.BOOL) { + // permission check + String permissionString = node.key().getNamespace() + '.' + node.key().getPath(); + Boolean permissionValue = permissionCheck(ctx, permissionString); + return node.cast(permissionValue); + } + + // assume anything non-boolean is an option lookup + String optionKey = node.key().toString(); + String optionValue = optionCheck(ctx, optionKey); + if (optionValue == null) { + return null; + } + + if (codec == Codec.STRING) { + return node.cast(optionValue); + } + if (codec == Codec.INT) { + return node.cast(Integer.parseInt(optionValue)); + } + + // attempt parse with JsonParser & JsonOps + JsonElement element = JsonParser.parseString(optionValue); + return codec.parse(JsonOps.INSTANCE, element).getOrThrow(); + } + + private CompletableFuture> onPrepareOfflinePlayer(PermissionContext ctx, MinecraftServer server) { + UUID uniqueId = ctx.uuid(); + String username = ctx.get(PermissionContextKey.NAME); + + return this.plugin.getStorage().loadUser(uniqueId, username).thenApply(user -> { + if (user == null) { + return null; + } + return mutableCtx -> mutableCtx.set(USER_KEY, user); + }); + } + + private static Boolean permissionCheck(PermissionContext ctx, String permission) { + Entity entity = ctx.get(PermissionContextKey.ENTITY); + if (entity instanceof MixinUser user) { + Tristate result = user.luckperms$hasPermission(permission); + return tristateToNullableBoolean(result); + } + + User user = ctx.get(USER_KEY); + if (user != null) { + Tristate result = user.getCachedData().getPermissionData().checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + return tristateToNullableBoolean(result); + } + + return null; + } + + private static String optionCheck(PermissionContext ctx, String optionKey) { + Entity entity = ctx.get(PermissionContextKey.ENTITY); + if (entity instanceof MixinUser user) { + return user.luckperms$getOption(optionKey); + } + + User user = ctx.get(USER_KEY); + if (user != null) { + return user.getCachedData().getMetaData().getMetaOrChatMetaValue(optionKey, CheckOrigin.PLATFORM_API); + } + + return null; + } + + private static Boolean tristateToNullableBoolean(Tristate value) { + return value == Tristate.UNDEFINED ? null : value.asBoolean(); + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricPermissionsListener.java b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricPermissionsListener.java new file mode 100644 index 000000000..a8f9ac8bd --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/FabricPermissionsListener.java @@ -0,0 +1,89 @@ +/* + * 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.listeners; + +import me.lucko.luckperms.fabric.event.SetupPlayerPermissionsEvent; +import me.lucko.luckperms.fabric.model.MixinUser; +import net.luckperms.api.util.Tristate; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.LevelBasedPermissionSet; +import net.minecraft.server.permissions.Permission; +import net.minecraft.server.permissions.PermissionLevel; +import net.minecraft.server.permissions.PermissionSet; +import org.jspecify.annotations.NonNull; + +/** + * Listener to route permission checks made via Minecraft's native permission predicate to LuckPerms. + */ +public class FabricPermissionsListener { + + public void registerListeners() { + SetupPlayerPermissionsEvent.EVENT.register(this::onSetupPlayerPermissions); + } + + private PermissionSet onSetupPlayerPermissions(ServerPlayer entity, PermissionSet defaults) { + return defaults instanceof LevelBasedPermissionSet levelBasedDefaults + ? new LuckPermsLevelBasedPermissionSet<>(entity, levelBasedDefaults) + : new LuckPermsPermissionSet<>(entity, defaults); + } + + private static class LuckPermsPermissionSet implements PermissionSet { + protected final ServerPlayer player; + protected final D delegate; + + LuckPermsPermissionSet(ServerPlayer player, D delegate) { + this.player = player; + this.delegate = delegate; + } + + @Override + public boolean hasPermission(@NonNull Permission permission) { + if (permission instanceof Permission.Atom) { + Identifier permissionId = ((Permission.Atom) permission).id(); + String permissionString = permissionId.getNamespace() + '.' + permissionId.getPath(); + + Tristate result = ((MixinUser) this.player).luckperms$hasPermission(permissionString); + if (result != Tristate.UNDEFINED) { + return result.asBoolean(); + } + } + + return this.delegate.hasPermission(permission); + } + } + + private static class LuckPermsLevelBasedPermissionSet extends LuckPermsPermissionSet implements LevelBasedPermissionSet { + LuckPermsLevelBasedPermissionSet(ServerPlayer player, D delegate) { + super(player, delegate); + } + + @Override + public @NonNull PermissionLevel level() { + return this.delegate.level(); + } + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/PermissionCheckListener.java b/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/PermissionCheckListener.java deleted file mode 100644 index 515bd5d7b..000000000 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/listeners/PermissionCheckListener.java +++ /dev/null @@ -1,93 +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.fabric.listeners; - -import me.lucko.fabric.api.permissions.v0.PermissionCheckEvent; -import me.lucko.luckperms.common.calculator.result.TristateResult; -import me.lucko.luckperms.common.query.QueryOptionsImpl; -import me.lucko.luckperms.common.verbose.VerboseCheckTarget; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent.Origin; -import me.lucko.luckperms.fabric.LPFabricPlugin; -import me.lucko.luckperms.fabric.model.MixinUser; - -import net.fabricmc.fabric.api.util.TriState; -import net.minecraft.command.CommandSource; -import net.minecraft.entity.Entity; -import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.server.network.ServerPlayerEntity; - -import org.checkerframework.checker.nullness.qual.NonNull; - -/** - * Listener to route permission checks made via fabric-permissions-api to LuckPerms. - */ -public class PermissionCheckListener { - private final LPFabricPlugin plugin; - - public PermissionCheckListener(LPFabricPlugin plugin) { - this.plugin = plugin; - } - - public void registerListeners() { - PermissionCheckEvent.EVENT.register(this::onPermissionCheck); - } - - private @NonNull TriState onPermissionCheck(CommandSource source, String permission) { - if (source instanceof ServerCommandSource) { - Entity entity = ((ServerCommandSource) source).getEntity(); - if (entity instanceof ServerPlayerEntity) { - return onPlayerPermissionCheck((ServerPlayerEntity) entity, permission); - } - } - return onOtherPermissionCheck(source, permission); - } - - private TriState onPlayerPermissionCheck(ServerPlayerEntity player, String permission) { - switch (((MixinUser) player).hasPermission(permission)) { - case TRUE: - return TriState.TRUE; - case FALSE: - return TriState.FALSE; - case UNDEFINED: - return TriState.DEFAULT; - default: - throw new AssertionError(); - } - } - - private TriState onOtherPermissionCheck(CommandSource source, String permission) { - if (source instanceof ServerCommandSource) { - String name = ((ServerCommandSource) source).getName(); - VerboseCheckTarget target = VerboseCheckTarget.internal(name); - - this.plugin.getVerboseHandler().offerPermissionCheckEvent(Origin.PLATFORM_PERMISSION_CHECK, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.UNDEFINED); - this.plugin.getPermissionRegistry().offer(permission); - } - - return TriState.DEFAULT; - } - -} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/FabricMessagingFactory.java b/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/FabricMessagingFactory.java index 9b207bc88..f86ac11a4 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/FabricMessagingFactory.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/FabricMessagingFactory.java @@ -29,11 +29,9 @@ import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; import me.lucko.luckperms.common.messaging.MessagingFactory; import me.lucko.luckperms.fabric.LPFabricPlugin; - import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; - import org.checkerframework.checker.nullness.qual.NonNull; public class FabricMessagingFactory extends MessagingFactory { diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/PluginMessageMessenger.java b/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/PluginMessageMessenger.java index 733c8a41d..a8de16001 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/PluginMessageMessenger.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/messaging/PluginMessageMessenger.java @@ -26,41 +26,38 @@ package me.lucko.luckperms.fabric.messaging; import com.google.common.collect.Iterables; - +import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; import me.lucko.luckperms.fabric.LPFabricPlugin; - -import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; -import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.luckperms.api.messenger.IncomingMessageConsumer; -import net.luckperms.api.messenger.Messenger; -import net.luckperms.api.messenger.message.OutgoingMessage; -import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.network.ServerPlayNetworkHandler; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.util.Identifier; - -import org.checkerframework.checker.nullness.qual.NonNull; +import net.minecraft.server.level.ServerPlayer; import java.util.Collection; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -public class PluginMessageMessenger implements Messenger, ServerPlayNetworking.PlayChannelHandler { - private static final Identifier CHANNEL = new Identifier("luckperms", "update"); +public class PluginMessageMessenger extends AbstractPluginMessageMessenger implements ServerPlayNetworking.PlayPayloadHandler { + private static final Identifier CHANNEL = Identifier.parse(AbstractPluginMessageMessenger.CHANNEL); private final LPFabricPlugin plugin; - private final IncomingMessageConsumer consumer; public PluginMessageMessenger(LPFabricPlugin plugin, IncomingMessageConsumer consumer) { + super(consumer); this.plugin = plugin; - this.consumer = consumer; } public void init() { - ServerPlayNetworking.registerGlobalReceiver(CHANNEL, this); + PayloadTypeRegistry.clientboundPlay().register(PluginMessagePayload.TYPE, PluginMessagePayload.CODEC); + PayloadTypeRegistry.serverboundPlay().register(PluginMessagePayload.TYPE, PluginMessagePayload.CODEC); + ServerPlayNetworking.registerGlobalReceiver(PluginMessagePayload.TYPE, this); } @Override @@ -69,7 +66,7 @@ public void close() { } @Override - public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { + protected void sendOutgoingMessage(byte[] buf) { AtomicReference taskRef = new AtomicReference<>(); SchedulerTask task = this.plugin.getBootstrap().getScheduler().asyncRepeating(() -> { MinecraftServer server = this.plugin.getBootstrap().getServer().orElse(null); @@ -77,15 +74,13 @@ public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { return; } - Collection players = server.getPlayerManager().getPlayerList(); - ServerPlayerEntity p = Iterables.getFirst(players, null); + Collection players = server.getPlayerList().getPlayers(); + ServerPlayer p = Iterables.getFirst(players, null); if (p == null) { return; } - PacketByteBuf buf = PacketByteBufs.create(); - buf.writeString(outgoingMessage.asEncodedString()); - ServerPlayNetworking.send(p, CHANNEL, buf); + ServerPlayNetworking.send(p, new PluginMessagePayload(buf)); SchedulerTask t = taskRef.getAndSet(null); if (t != null) { @@ -96,8 +91,32 @@ public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { } @Override - public void receive(MinecraftServer server, ServerPlayerEntity entity, ServerPlayNetworkHandler netHandler, PacketByteBuf buf, PacketSender packetSender) { - String msg = buf.readString(); - this.consumer.consumeIncomingMessageAsString(msg); + public void receive(PluginMessagePayload payload, ServerPlayNetworking.Context context) { + handleIncomingMessage(payload.data); + } + + public static class PluginMessagePayload implements CustomPacketPayload { + public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(CHANNEL); + public static final StreamCodec CODEC = StreamCodec.ofMember(PluginMessagePayload::write, PluginMessagePayload::new).cast(); + + private final byte[] data; + + private PluginMessagePayload(byte[] data) { + this.data = data; + } + + private PluginMessagePayload(FriendlyByteBuf buf) { + this.data = new byte[buf.readableBytes()]; + buf.readBytes(this.data); + } + + private void write(FriendlyByteBuf buf) { + buf.writeBytes(this.data); + } + + @Override + public Type type() { + return TYPE; + } } } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ClientSettingsC2SPacketAccessor.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/CommandSourceStackAccessor.java similarity index 81% rename from fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ClientSettingsC2SPacketAccessor.java rename to fabric/src/main/java/me/lucko/luckperms/fabric/mixin/CommandSourceStackAccessor.java index 929006fca..bab57ece7 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ClientSettingsC2SPacketAccessor.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/CommandSourceStackAccessor.java @@ -25,15 +25,18 @@ package me.lucko.luckperms.fabric.mixin; -import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket; - +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; -@Mixin(ClientSettingsC2SPacket.class) -public interface ClientSettingsC2SPacketAccessor { +/** + * Accessor mixin to provide access to the underlying {@link CommandSource} + */ +@Mixin(CommandSourceStack.class) +public interface CommandSourceStackAccessor { - @Accessor("language") - String getLanguage(); + @Accessor("source") + CommandSource getSource(); } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/CommandsMixin.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/CommandsMixin.java new file mode 100644 index 000000000..eac4357fb --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/CommandsMixin.java @@ -0,0 +1,45 @@ +/* + * 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 com.mojang.brigadier.ParseResults; +import me.lucko.luckperms.fabric.event.PreExecuteCommandCallback; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Commands.class) +public class CommandsMixin { + @Inject(at = @At("HEAD"), method = "performCommand", cancellable = true) + private void commandExecuteCallback(ParseResults parseResults, String command, CallbackInfo ci) { + if (!PreExecuteCommandCallback.EVENT.invoker().onPreExecuteCommand(parseResults.getContext().getSource(), command)) { + ci.cancel(); + } + } +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginNetworkHandlerAccessor.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginPacketListenerImplAccessor.java similarity index 86% rename from fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginNetworkHandlerAccessor.java rename to fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginPacketListenerImplAccessor.java index 9d0db0c74..3d4a393bc 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginNetworkHandlerAccessor.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerLoginPacketListenerImplAccessor.java @@ -26,9 +26,7 @@ package me.lucko.luckperms.fabric.mixin; import com.mojang.authlib.GameProfile; - -import net.minecraft.server.network.ServerLoginNetworkHandler; - +import net.minecraft.server.network.ServerLoginPacketListenerImpl; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @@ -36,10 +34,10 @@ * Accessor mixin to provide access to the underlying {@link GameProfile} during the server * login handling. */ -@Mixin(ServerLoginNetworkHandler.class) -public interface ServerLoginNetworkHandlerAccessor { +@Mixin(ServerLoginPacketListenerImpl.class) +public interface ServerLoginPacketListenerImplAccessor { - @Accessor("profile") - GameProfile getGameProfile(); + @Accessor("authenticatedProfile") + GameProfile getAuthenticatedProfile(); } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerMixin.java similarity index 50% rename from fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java rename to fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerMixin.java index d7b5c3a21..74b14ffb1 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerEntityMixin.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/mixin/ServerPlayerMixin.java @@ -25,88 +25,71 @@ package me.lucko.luckperms.fabric.mixin; +import me.lucko.luckperms.common.cacheddata.type.MetaCache; import me.lucko.luckperms.common.cacheddata.type.PermissionCache; -import me.lucko.luckperms.common.context.QueryOptionsCache; -import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.context.manager.QueryOptionsSupplier; import me.lucko.luckperms.common.model.User; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import me.lucko.luckperms.fabric.context.FabricContextManager; -import me.lucko.luckperms.fabric.event.PlayerChangeWorldCallback; +import me.lucko.luckperms.fabric.event.SetupPlayerPermissionsEvent; import me.lucko.luckperms.fabric.model.MixinUser; - import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; -import net.minecraft.network.packet.c2s.play.ClientSettingsC2SPacket; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.server.world.ServerWorld; - +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.permissions.PermissionSet; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.Locale; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; /** - * Mixin into {@link ServerPlayerEntity} to store LP caches and implement {@link MixinUser}. - * - *

    This mixin is also temporarily used to implement our internal PlayerChangeWorldCallback, - * until a similar event is added to Fabric itself.

    + * Mixin into {@link ServerPlayer} to store LP caches and implement {@link MixinUser}. */ -@Mixin(ServerPlayerEntity.class) -public abstract class ServerPlayerEntityMixin implements MixinUser { +@Mixin(ServerPlayer.class) +public abstract class ServerPlayerMixin implements MixinUser { /** Cache a reference to the LP {@link User} instance loaded for this player */ + @Unique private User luckperms$user; /** * Hold a QueryOptionsCache instance on the player itself, so we can just cast instead of * having to maintain a map of Player->Cache. */ - private QueryOptionsCache luckperms$queryOptions; - - // Cache player locale - private Locale luckperms$locale; - - // Used by PlayerChangeWorldCallback hook below. - @Shadow public abstract ServerWorld getServerWorld(); + @Unique + private QueryOptionsSupplier luckperms$queryOptions; @Override - public User getLuckPermsUser() { + public User luckperms$getUser() { return this.luckperms$user; } @Override - public QueryOptionsCache getQueryOptionsCache() { - return this.luckperms$queryOptions; - } - - @Override - public QueryOptionsCache getQueryOptionsCache(FabricContextManager contextManager) { + public QueryOptionsSupplier luckperms$getQueryOptionsCache(FabricContextManager contextManager) { if (this.luckperms$queryOptions == null) { - this.luckperms$queryOptions = contextManager.newQueryOptionsCache((ServerPlayerEntity) (Object) this); + this.luckperms$queryOptions = contextManager.createQueryOptionsSupplier((ServerPlayer) (Object) this); } return this.luckperms$queryOptions; } @Override - public Locale getCachedLocale() { - return this.luckperms$locale; - } + public void luckperms$initializePermissions(User user) { + if (user == null) { + return; + } - @Override - public void initializePermissions(User user) { this.luckperms$user = user; // ensure query options cache is initialised too. if (this.luckperms$queryOptions == null) { - this.getQueryOptionsCache((FabricContextManager) user.getPlugin().getContextManager()); + this.luckperms$getQueryOptionsCache((FabricContextManager) user.getPlugin().getContextManager()); } } @Override - public Tristate hasPermission(String permission) { + public Tristate luckperms$hasPermission(String permission) { if (permission == null) { throw new NullPointerException("permission"); } @@ -114,11 +97,11 @@ public Tristate hasPermission(String permission) { // "fake" players will have our mixin, but won't have been initialised. return Tristate.UNDEFINED; } - return hasPermission(permission, this.luckperms$queryOptions.getQueryOptions()); + return luckperms$hasPermission(permission, this.luckperms$queryOptions.getQueryOptions()); } @Override - public Tristate hasPermission(String permission, QueryOptions queryOptions) { + public Tristate luckperms$hasPermission(String permission, QueryOptions queryOptions) { if (permission == null) { throw new NullPointerException("permission"); } @@ -133,27 +116,54 @@ public Tristate hasPermission(String permission, QueryOptions queryOptions) { } PermissionCache data = user.getCachedData().getPermissionData(queryOptions); - return data.checkPermission(permission, PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK).result(); + return data.checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + } + + @Override + public String luckperms$getOption(String key) { + if (key == null) { + throw new NullPointerException("key"); + } + if (this.luckperms$user == null || this.luckperms$queryOptions == null) { + // "fake" players will have our mixin, but won't have been initialised. + return null; + } + return luckperms$getOption(key, this.luckperms$queryOptions.getQueryOptions()); } + @Override + public String luckperms$getOption(String key, QueryOptions queryOptions) { + if (key == null) { + throw new NullPointerException("key"); + } + if (queryOptions == null) { + throw new NullPointerException("queryOptions"); + } - @Inject(at = @At("TAIL"), method = "copyFrom") - private void luckperms_copyFrom(ServerPlayerEntity oldPlayer, boolean alive, CallbackInfo ci) { - MixinUser oldMixin = (MixinUser) oldPlayer; - this.luckperms$user = oldMixin.getLuckPermsUser(); - this.luckperms$queryOptions = oldMixin.getQueryOptionsCache(); - this.luckperms$queryOptions.invalidate(); - this.luckperms$locale = oldMixin.getCachedLocale(); + final User user = this.luckperms$user; + if (user == null || this.luckperms$queryOptions == null) { + // "fake" players will have our mixin, but won't have been initialised. + return null; + } + + MetaCache cache = user.getCachedData().getMetaData(queryOptions); + return cache.getMetaOrChatMetaValue(key, CheckOrigin.PLATFORM_API); } - @Inject(at = @At("HEAD"), method = "setClientSettings") - private void luckperms_setClientSettings(ClientSettingsC2SPacket information, CallbackInfo ci) { - String language = ((ClientSettingsC2SPacketAccessor) information).getLanguage(); - this.luckperms$locale = TranslationManager.parseLocale(language); + @Inject(at = @At("TAIL"), method = "restoreFrom") + private void luckperms$restoreFrom(ServerPlayer oldPlayer, boolean alive, CallbackInfo ci) { + MixinUser oldMixin = (MixinUser) oldPlayer; + luckperms$initializePermissions(oldMixin.luckperms$getUser()); } - @Inject(at = @At("TAIL"), method = "worldChanged") - private void luckperms_onChangeDimension(ServerWorld targetWorld, CallbackInfo ci) { - PlayerChangeWorldCallback.EVENT.invoker().onChangeWorld(this.getServerWorld(), targetWorld, (ServerPlayerEntity) (Object) this); + @Inject(at = @At("RETURN"), method = "permissions", cancellable = true) + private void luckperms$permissions(CallbackInfoReturnable cir) { + ServerPlayer entity = (ServerPlayer) (Object) this; + PermissionSet set = cir.getReturnValue(); + + PermissionSet newSet = SetupPlayerPermissionsEvent.EVENT.invoker().onSetupPlayerPermissions(entity, set); + if (newSet != set) { + cir.setReturnValue(newSet); + } } } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java b/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java index 4215908d6..3d072011d 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/model/MixinUser.java @@ -25,47 +25,43 @@ package me.lucko.luckperms.fabric.model; -import me.lucko.luckperms.common.context.QueryOptionsCache; +import me.lucko.luckperms.common.context.manager.QueryOptionsSupplier; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.fabric.context.FabricContextManager; - import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; -import net.minecraft.server.network.ServerPlayerEntity; - -import java.util.Locale; +import net.minecraft.server.level.ServerPlayer; /** - * Mixin interface for {@link ServerPlayerEntity} implementing {@link User} related - * caches and functions. + * Mixin interface for {@link ServerPlayer} implementing {@link User} related caches and functions. */ public interface MixinUser { - User getLuckPermsUser(); - - QueryOptionsCache getQueryOptionsCache(); + User luckperms$getUser(); /** - * Gets (or creates using the manager) the objects {@link QueryOptionsCache}. + * Gets (or creates using the manager) the objects {@link QueryOptionsSupplier}. * * @param contextManager the contextManager * @return the cache */ - QueryOptionsCache getQueryOptionsCache(FabricContextManager contextManager); - - Locale getCachedLocale(); + QueryOptionsSupplier luckperms$getQueryOptionsCache(FabricContextManager contextManager); /** * Initialises permissions for this player using the given {@link User}. * * @param user the user */ - void initializePermissions(User user); + void luckperms$initializePermissions(User user); // methods to perform permission checks using the User instance initialised on login - Tristate hasPermission(String permission); + Tristate luckperms$hasPermission(String permission); + + Tristate luckperms$hasPermission(String permission, QueryOptions queryOptions); + + String luckperms$getOption(String key); - Tristate hasPermission(String permission, QueryOptions queryOptions); + String luckperms$getOption(String key, QueryOptions queryOptions); } diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/placeholder/FabricPlaceholderApiIntegration.java b/fabric/src/main/java/me/lucko/luckperms/fabric/placeholder/FabricPlaceholderApiIntegration.java new file mode 100644 index 000000000..b5b9deabe --- /dev/null +++ b/fabric/src/main/java/me/lucko/luckperms/fabric/placeholder/FabricPlaceholderApiIntegration.java @@ -0,0 +1,98 @@ +/* + * 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.placeholder; + +import eu.pb4.placeholders.api.ParserContext; +import eu.pb4.placeholders.api.PlaceholderResult; +import eu.pb4.placeholders.api.Placeholders; +import eu.pb4.placeholders.api.ServerPlaceholderContext; +import eu.pb4.placeholders.api.parsers.NodeParser; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.placeholders.Placeholder; +import me.lucko.luckperms.common.placeholders.PlaceholderContext; +import me.lucko.luckperms.common.placeholders.PlaceholderRegistry; +import me.lucko.luckperms.fabric.LPFabricPlugin; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerPlayer; + +public class FabricPlaceholderApiIntegration { + private final LPFabricPlugin plugin; + + public FabricPlaceholderApiIntegration(LPFabricPlugin plugin) { + this.plugin = plugin; + } + + public void register() { + for (Placeholder placeholder : PlaceholderRegistry.getAll()) { + Placeholders.registerServer( + Identifier.fromNamespaceAndPath("luckperms", placeholder.id()), + new Handler(this.plugin, placeholder) + ); + } + } + + private static final NodeParser NODE_PARSER = NodeParser.builder().legacyAll().simplifiedTextFormat().quickText().build(); + + private record Handler(LPFabricPlugin plugin, Placeholder placeholder) implements eu.pb4.placeholders.api.Placeholder.Handler { + @Override + public PlaceholderResult onPlaceholderRequest(ServerPlaceholderContext context, String argument) { + ServerPlayer player = context.serverPlayer(); + User user = this.plugin.getUserManager().getIfLoaded(player.getUUID()); + if (user == null) { + return PlaceholderResult.invalid("Unable to find corresponding user for UUID: " + player.getUUID()); + } + + QueryOptions queryOptions = this.plugin.getContextManager().getQueryOptions(player); + PlaceholderContext ctx = new PlaceholderContext(this.plugin.getApiProvider(), user.getApiProxy(), queryOptions); + + String result; + if (this.placeholder instanceof Placeholder.Basic basic) { + result = basic.resolve(ctx); + } else if (this.placeholder instanceof Placeholder.UsingArgument usingArg) { + result = usingArg.resolve(ctx.withArgument(argument == null ? "" : argument)); + } else { + throw new IllegalStateException("Unknown placeholder type: " + this.placeholder.getClass()); + } + + return toResult(parseText(result)); + } + + private Component parseText(String input) { + if (input == null) { + return null; + } + return NODE_PARSER.parseComponent(input, ParserContext.of()); + } + + private static PlaceholderResult toResult(Component component) { + return component == null + ? PlaceholderResult.invalid() + : PlaceholderResult.value(component); + } + } +} diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 8a32a743b..87a2d2c5c 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": { @@ -30,12 +30,15 @@ "luckperms.mixins.json" ], "depends": { - "fabricloader": ">=0.9.0", + "fabricloader": ">=0.18.4", + "minecraft": ">=26.1", "fabric-api-base": "*", - "fabric-command-api-v1": "*", + "fabric-command-api-v2": "*", "fabric-lifecycle-events-v1": "*", - "fabric-networking-v0": "*", - "fabric-permissions-api-v0": "*" + "fabric-networking-api-v1": "*", + "fabric-entity-events-v1": "*", + "fabric-permission-api-v1": "*", + "fabric-permissions-api-v0": ">=0.7.0" }, "custom": { "modmenu:api": true diff --git a/fabric/src/main/resources/luckperms.conf b/fabric/src/main/resources/luckperms.conf index fe8b50d12..65b27572f 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 | # @@ -156,15 +156,25 @@ data { } # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). # - Change this if you want to use different tables for different servers. table-prefix = "luckperms_" - # The prefix to use for all LuckPerms collections. Change this if you want to use different - # collections for different servers. The default is no prefix. + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. mongodb-collection-prefix = "" - # MongoDB ClientConnectionURI for use with replica sets and custom connection options - # - See https://docs.mongodb.com/manual/reference/connection-string/ + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ mongodb-connection-uri = "" } @@ -234,6 +244,9 @@ watch-files = true # below. # => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be # configured below. +# => nats Uses Nats pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. # => auto Attempts to automatically setup a messaging service using redis or sql. messaging-service = "auto" @@ -252,10 +265,32 @@ 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" + username = "" + password = "" + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel { + enabled = false + master = "mymaster" + addresses = ["localhost:26379"] + username = "" + password = "" + } +} + +# Settings for nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats { + enabled = false + address = "localhost" + username = "" password = "" + token = "" } # Settings for RabbitMQ. @@ -503,7 +538,7 @@ apply-wildcards = true # - That being: If a user has been granted "example", then the player should have also be # automatically granted "example.function", "example.another", "example.deeper.nesting", # and so on. -apply-sponge-implicit-wildcards=true +apply-sponge-implicit-wildcards = true # If the plugin should parse regex permissions. # @@ -526,6 +561,13 @@ integrated-server-owner-bypasses-checks = true # | Extra settings | # # +----------------------------------------------------------------------------------------------+ # +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators = [] + # Allows you to set "aliases" for the worlds sent forward for context calculation. # # - These aliases are provided in addition to the real world name. Applied recursively. @@ -555,6 +597,30 @@ group-weight { # | | # # +----------------------------------------------------------------------------------------------+ # +# +----------------------------------------------------------------------------------------------+ # +# | Server Operator (OP) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls whether server operators should exist at all. +# +# - When set to 'false', all players will be de-opped, and the /op and /deop commands will be +# disabled. Note that vanilla features like the spawn-protection require an operator on the +# server to work. +enable-ops = true + +# Enables or disables a special permission based system in LuckPerms for controlling OP status. +# +# - If set to true, any user with the permission "luckperms.autoop" will automatically be granted +# server operator status. This permission can be inherited, or set on specific servers/worlds, +# temporarily, etc. +# - Additionally, setting this to true will force the "enable-ops" option above to false. All users +# will be de-opped unless they have the permission node, and the op/deop commands will be +# disabled. +# - It is recommended that you use this option instead of assigning a single '*' permission. +# - However, on Fabric this setting can be used as a "pseudo" root wildcard, as many mods support +# the operator system over permissions. +auto-op = false + # +----------------------------------------------------------------------------------------------+ # # | Miscellaneous (and rarely used) settings | # # +----------------------------------------------------------------------------------------------+ # @@ -578,11 +644,52 @@ allow-invalid-usernames = false # - If automation is needed, users should prefer using the LuckPerms API. skip-bulkupdate-confirmation = false +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate = false + # If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. # # - When this happens, the plugin will set their primary group back to default. prevent-primary-group-removal = false +# If LuckPerms should update the list of commands sent to the client when permissions are changed. +update-client-command-list = true + # If LuckPerms should attempt to resolve Vanilla command target selectors for LP commands. -# See here for more info: https://minecraft.gamepedia.com/Commands#Target_selectors +# See here for more info: https://minecraft.wiki/w/Target_selectors resolve-command-selectors = false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode { + players = false + console = false +} + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands { + players = false + console = false +} diff --git a/fabric/src/main/resources/luckperms.mixins.json b/fabric/src/main/resources/luckperms.mixins.json index b83dfbca7..b5c7dbb7e 100644 --- a/fabric/src/main/resources/luckperms.mixins.json +++ b/fabric/src/main/resources/luckperms.mixins.json @@ -3,13 +3,14 @@ "package": "me.lucko.luckperms.fabric.mixin", "compatibilityLevel": "JAVA_8", "mixins": [ - "ClientSettingsC2SPacketAccessor", - "ServerLoginNetworkHandlerAccessor", - "ServerPlayerEntityMixin" + "CommandsMixin", + "CommandSourceStackAccessor", + "ServerLoginPacketListenerImplAccessor", + "ServerPlayerMixin" ], "client": [ ], "injectors": { "defaultRequire": 1 } -} \ No newline at end of file +} diff --git a/forge/build.gradle b/forge/build.gradle new file mode 100644 index 000000000..b38668902 --- /dev/null +++ b/forge/build.gradle @@ -0,0 +1,54 @@ +plugins { + alias(libs.plugins.shadow) + alias(libs.plugins.forgegradle) +} + +tasks.withType(JavaCompile).configureEach { + options.release = 25 +} + +repositories { + minecraft.mavenizer(it) + maven fg.forgeMaven + maven fg.minecraftLibsMaven +} + +dependencies { + implementation minecraft.dependency("net.minecraftforge:forge:${minecraftVersion}-${forgeVersion}") + annotationProcessor 'net.minecraftforge:eventbus-validator:7.0.1' + implementation project(':common') + implementation project(':common:minecraft') + compileOnly project(':common:loader-utils') +} + +shadowJar { + archiveFileName = "luckperms-forge.jarinjar" + + dependencies { + include(dependency('me.lucko.luckperms:.*')) + } + + relocate 'net.kyori.adventure', 'me.lucko.luckperms.lib.adventure' + relocate 'net.kyori.event', 'me.lucko.luckperms.lib.eventbus' + relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine' + relocate 'okio', 'me.lucko.luckperms.lib.okio' + relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3' + relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy' + 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 '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' + relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' + relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' + relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' + relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' + relocate 'org.yaml.snakeyaml', 'me.lucko.luckperms.lib.yaml' +} + +artifacts { + archives shadowJar +} diff --git a/forge/gradle.properties b/forge/gradle.properties new file mode 100644 index 000000000..d40aff489 --- /dev/null +++ b/forge/gradle.properties @@ -0,0 +1,2 @@ +minecraftVersion=26.2 +forgeVersion=65.0.0 \ No newline at end of file diff --git a/forge/loader/build.gradle b/forge/loader/build.gradle new file mode 100644 index 000000000..b80a53983 --- /dev/null +++ b/forge/loader/build.gradle @@ -0,0 +1,62 @@ +plugins { + alias(libs.plugins.shadow) + alias(libs.plugins.forgegradle) + id("java-library") +} + +tasks.withType(JavaCompile).configureEach { + options.release = 25 +} + +repositories { + minecraft.mavenizer(it) + maven fg.forgeMaven + maven fg.minecraftLibsMaven +} + +dependencies { + implementation minecraft.dependency("net.minecraftforge:forge:${minecraftVersion}-${forgeVersion}") + implementation project(':api') + implementation project(':common:loader-utils') +} + +build { + dependsOn(":forge:build") +} + +jar { + manifest { + attributes( + 'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), + 'Implementation-Title': 'LuckPerms', + 'Implementation-Vendor': 'LuckPerms', + 'Implementation-Version': project.ext.fullVersion, + 'Specification-Title': 'luckperms', + 'Specification-Vendor': 'LuckPerms', + 'Specification-Version': '1' + ) + } +} + +processResources { + filesMatching('META-INF/mods.toml') { + expand 'version': project.ext.fullVersion + } +} + +shadowJar { + archiveFileName = "LuckPerms-Forge-${project.ext.fullVersion}.jar" + + from { + project(':forge').tasks.shadowJar.archiveFile + } + + dependencies { + include(dependency('net.luckperms:.*')) + include(dependency('me.lucko.luckperms:.*')) + } +} + +artifacts { + archives shadowJar +} diff --git a/forge/loader/src/main/java/me/lucko/luckperms/forge/loader/ForgeLoaderPlugin.java b/forge/loader/src/main/java/me/lucko/luckperms/forge/loader/ForgeLoaderPlugin.java new file mode 100644 index 000000000..bed3d14bc --- /dev/null +++ b/forge/loader/src/main/java/me/lucko/luckperms/forge/loader/ForgeLoaderPlugin.java @@ -0,0 +1,77 @@ +/* + * 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.forge.loader; + +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import net.minecraftforge.fml.IExtensionPoint; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.javafmlmod.FMLModContainer; +import net.minecraftforge.fml.loading.FMLEnvironment; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.function.Supplier; + +@Mod(value = "luckperms") +public class ForgeLoaderPlugin implements Supplier { + private static final Logger LOGGER = LogManager.getLogger("luckperms"); + + private static final String JAR_NAME = "luckperms-forge.jarinjar"; + private static final String BOOTSTRAP_CLASS = "me.lucko.luckperms.forge.LPForgeBootstrap"; + + private final FMLModContainer container; + + private JarInJarClassLoader loader; + private LoaderBootstrap plugin; + + public ForgeLoaderPlugin(FMLJavaModLoadingContext ctx) { + this.container = ctx.getContainer(); + ctx.registerDisplayTest(IExtensionPoint.DisplayTest.IGNORE_SERVER_VERSION); + + if (FMLEnvironment.dist.isClient()) { + LOGGER.info("Skipping LuckPerms init (not supported on the client!)"); + return; + } + + this.loader = new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME); + FMLCommonSetupEvent.getBus(ctx.getModBusGroup()).addListener(this::onCommonSetup); + } + + @Override + public ModContainer get() { + return this.container; + } + + public void onCommonSetup(FMLCommonSetupEvent event) { + this.plugin = this.loader.instantiatePlugin(BOOTSTRAP_CLASS, Supplier.class, this); + this.plugin.onLoad(); + } + +} diff --git a/forge/loader/src/main/resources/META-INF/mods.toml b/forge/loader/src/main/resources/META-INF/mods.toml new file mode 100644 index 000000000..4e2a50c34 --- /dev/null +++ b/forge/loader/src/main/resources/META-INF/mods.toml @@ -0,0 +1,14 @@ +modLoader="javafml" +loaderVersion="[41,)" +license="MIT" +issueTrackerURL="https://github.com/LuckPerms/LuckPerms/issues" + +[[mods]] + modId="luckperms" + version="${version}" + displayName="LuckPerms" + displayURL="https://luckperms.net/" + logoFile="luckperms.png" + credits="Luck" + authors="Luck" + description="A permissions plugin for Minecraft servers." \ No newline at end of file diff --git a/forge/loader/src/main/resources/luckperms.png b/forge/loader/src/main/resources/luckperms.png new file mode 100644 index 000000000..2e0ea669a Binary files /dev/null and b/forge/loader/src/main/resources/luckperms.png differ diff --git a/forge/loader/src/main/resources/pack.mcmeta b/forge/loader/src/main/resources/pack.mcmeta new file mode 100644 index 000000000..f6749d3ff --- /dev/null +++ b/forge/loader/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "LuckPerms resources", + "pack_format": 9 + } +} \ No newline at end of file diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeCommandExecutor.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeCommandExecutor.java new file mode 100644 index 000000000..6ae4d4101 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeCommandExecutor.java @@ -0,0 +1,48 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.minecraft.command.MinecraftCommandExecutor; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.eventbus.api.listener.SubscribeEvent; + +public class ForgeCommandExecutor extends MinecraftCommandExecutor { + public ForgeCommandExecutor(LPForgePlugin plugin) { + super(plugin); + } + + @SubscribeEvent + public void onRegisterCommands(RegisterCommandsEvent event) { + register(event.getDispatcher()); + } + + // not used - workaround for the idiotic Forge limitation that requires listeners using the @SubscribeEvent annotation to have at least two listener methods + @SubscribeEvent + public void onServerStarted(ServerStartedEvent event) { + + } +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeConfigAdapter.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeConfigAdapter.java new file mode 100644 index 000000000..2d9464f24 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeConfigAdapter.java @@ -0,0 +1,46 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.config.generic.adapter.ConfigurateConfigAdapter; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.hocon.HoconConfigurationLoader; +import ninja.leaping.configurate.loader.ConfigurationLoader; + +import java.nio.file.Path; + +public class ForgeConfigAdapter extends ConfigurateConfigAdapter { + public ForgeConfigAdapter(LuckPermsPlugin plugin, Path path) { + super(plugin, path); + } + + @Override + protected ConfigurationLoader createLoader(Path path) { + return HoconConfigurationLoader.builder().setPath(path).build(); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeEventBus.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeEventBus.java new file mode 100644 index 000000000..8efce6c63 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeEventBus.java @@ -0,0 +1,49 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; + +public class ForgeEventBus extends AbstractEventBus { + public ForgeEventBus(LuckPermsPlugin plugin, LuckPermsApiProvider apiProvider) { + super(plugin, apiProvider); + } + + @Override + protected ModContainer checkPlugin(Object mod) throws IllegalArgumentException { + ModContainer modContainer = ModList.getModContainerByObject(mod).orElse(null); + if (modContainer != null) { + return modContainer; + } + + throw new IllegalArgumentException("Object " + mod + " (" + mod.getClass().getName() + ") is not a ModContainer."); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java new file mode 100644 index 000000000..f18ea2aec --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/ForgeSenderFactory.java @@ -0,0 +1,67 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.minecraft.MinecraftSenderFactory; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import me.lucko.luckperms.common.verbose.VerboseCheckTarget; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.level.ServerPlayer; + +public class ForgeSenderFactory extends MinecraftSenderFactory { + public ForgeSenderFactory(LPForgePlugin plugin) { + super(plugin); + } + + @Override + protected CommandSource getSource(CommandSourceStack sender) { + return sender.source; + } + + @Override + protected Tristate getPermissionValue(CommandSourceStack commandSource, String node) { + if (commandSource.getEntity() instanceof ServerPlayer player) { + User user = getPlugin().getUserManager().getIfLoaded(player.getUUID()); + if (user == null) { + return Tristate.UNDEFINED; + } + + QueryOptions queryOptions = getPlugin().getContextManager().getQueryOptions(player); + return user.getCachedData().getPermissionData(queryOptions).checkPermission(node, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + } + + VerboseCheckTarget target = VerboseCheckTarget.internal(commandSource.getTextName()); + getPlugin().getVerboseHandler().offerPermissionCheckEvent(CheckOrigin.PLATFORM_API_HAS_PERMISSION, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, node, TristateResult.UNDEFINED); + getPlugin().getPermissionRegistry().offer(node); + return Tristate.UNDEFINED; + } +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/LPForgeBootstrap.java b/forge/src/main/java/me/lucko/luckperms/forge/LPForgeBootstrap.java new file mode 100644 index 000000000..14cf86261 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/LPForgeBootstrap.java @@ -0,0 +1,258 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsBootstrap; +import me.lucko.luckperms.common.minecraft.MinecraftSchedulerAdapter; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; +import me.lucko.luckperms.common.plugin.logging.Log4jPluginLogger; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; +import me.lucko.luckperms.common.util.BuildInfo; +import net.luckperms.api.platform.Platform; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.event.server.ServerAboutToStartEvent; +import net.minecraftforge.event.server.ServerStoppedEvent; +import net.minecraftforge.event.server.ServerStoppingEvent; +import net.minecraftforge.eventbus.api.bus.BusGroup; +import net.minecraftforge.eventbus.api.listener.EventListener; +import net.minecraftforge.eventbus.api.listener.Priority; +import net.minecraftforge.eventbus.api.listener.SubscribeEvent; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.loading.FMLPaths; +import net.minecraftforge.forgespi.language.IModInfo; +import org.apache.logging.log4j.LogManager; +import org.apache.maven.artifact.versioning.ArtifactVersion; + +import java.lang.invoke.MethodHandles; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.function.Supplier; + +/** + * Bootstrap plugin for LuckPerms running on Forge. + */ +public final class LPForgeBootstrap extends MinecraftLuckPermsBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { + public static final String ID = "luckperms"; + + /** + * The plugin loader + */ + private final Supplier loader; + + /** + * The plugin logger + */ + private final PluginLogger logger; + + /** + * A scheduler adapter for the platform + */ + private final MinecraftSchedulerAdapter schedulerAdapter; + + /** + * The plugin class path appender + */ + private final ClassPathAppender classPathAppender; + + /** + * A list of all event listerners registered by the plugin. + */ + private final List listeners = new ArrayList<>(); + + /** + * The plugin instance + */ + private final LPForgePlugin plugin; + + /** + * The time when the plugin was enabled + */ + private Instant startTime; + + // load/enable latches + private final CountDownLatch loadLatch = new CountDownLatch(1); + private final CountDownLatch enableLatch = new CountDownLatch(1); + + /** + * The Minecraft server instance + */ + private MinecraftServer server; + + public LPForgeBootstrap(Supplier loader) { + this.loader = loader; + this.logger = new Log4jPluginLogger(LogManager.getLogger(LPForgeBootstrap.ID)); + this.schedulerAdapter = new MinecraftSchedulerAdapter(this); + this.classPathAppender = new JarInJarClassPathAppender(getClass().getClassLoader()); + this.plugin = new LPForgePlugin(this); + } + + // provide adapters + + @Override + public Object getLoader() { + return this.loader; + } + + @Override + public PluginLogger getPluginLogger() { + return this.logger; + } + + @Override + public MinecraftSchedulerAdapter getScheduler() { + return this.schedulerAdapter; + } + + @Override + public ClassPathAppender getClassPathAppender() { + return this.classPathAppender; + } + + public void registerListeners(Object target) { + Collection listeners = BusGroup.DEFAULT.register(MethodHandles.lookup(), target); + this.listeners.addAll(listeners); + } + + public void unregisterListeners() { + if (this.listeners.isEmpty()) { + return; + } + BusGroup.DEFAULT.unregister(this.listeners); + this.listeners.clear(); + } + + // lifecycle + + @Override + public void onLoad() { // called by the loader on FMLCommonSetupEvent + this.startTime = Instant.now(); + try { + this.plugin.load(); + } finally { + this.loadLatch.countDown(); + } + + registerListeners(this); + this.plugin.registerEarlyListeners(); + } + + @SubscribeEvent(priority = Priority.HIGHEST) + public void onServerAboutToStart(ServerAboutToStartEvent event) { + this.server = event.getServer(); + try { + this.plugin.enable(); + } finally { + this.enableLatch.countDown(); + } + } + + @SubscribeEvent(priority = Priority.LOWEST) + public void onServerStopping(ServerStoppingEvent event) { + this.plugin.disable(); + unregisterListeners(); + this.server = null; + } + + @SubscribeEvent(priority = Priority.LOWEST) + public void onServerStopped(ServerStoppedEvent event) { + if (this.server == null) { + return; + } + + this.plugin.disable(); + unregisterListeners(); + this.server = null; + } + + @Override + public CountDownLatch getLoadLatch() { + return this.loadLatch; + } + + @Override + public CountDownLatch getEnableLatch() { + return this.enableLatch; + } + + @Override + public Optional getServer() { + return Optional.ofNullable(this.server); + } + + // provide information about the plugin + + @Override + public String getVersion() { + return BuildInfo.VERSION; + } + + @Override + public Instant getStartupTime() { + return this.startTime; + } + + // provide information about the platform + + @Override + public Platform.Type getType() { + return Platform.Type.FORGE; + } + + @Override + public String getServerBrand() { + return ModList.getModContainerById("forge") + .map(ModContainer::getModInfo) + .map(IModInfo::getDisplayName) + .orElse("null"); + } + + @Override + public String getServerVersion() { + String forgeVersion = ModList.getModContainerById("forge") + .map(ModContainer::getModInfo) + .map(IModInfo::getVersion) + .map(ArtifactVersion::toString) + .orElse("null"); + + return getServer().map(MinecraftServer::getServerVersion).orElse("null") + "-" + forgeVersion; + } + + @Override + public Path getDataDirectory() { + return FMLPaths.CONFIGDIR.get().resolve(LPForgeBootstrap.ID).toAbsolutePath(); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/LPForgePlugin.java b/forge/src/main/java/me/lucko/luckperms/forge/LPForgePlugin.java new file mode 100644 index 000000000..a6578ae3a --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/LPForgePlugin.java @@ -0,0 +1,168 @@ +/* + * 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.forge; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import me.lucko.luckperms.common.dependencies.Dependency; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsPlugin; +import me.lucko.luckperms.common.minecraft.listeners.MinecraftAutoOpListener; +import me.lucko.luckperms.common.minecraft.listeners.MinecraftCommandListUpdater; +import me.lucko.luckperms.forge.context.ForgeContextManager; +import me.lucko.luckperms.forge.context.ForgePlayerCalculator; +import me.lucko.luckperms.forge.listeners.ForgeConnectionListener; +import me.lucko.luckperms.forge.listeners.ForgePlatformListener; +import me.lucko.luckperms.forge.messaging.ForgeMessagingFactory; +import me.lucko.luckperms.forge.messaging.PluginMessageMessenger; +import me.lucko.luckperms.forge.service.ForgePermissionHandlerListener; +import net.luckperms.api.LuckPerms; +import net.minecraftforge.fml.ModContainer; + +import java.util.Set; + +/** + * LuckPerms implementation for Forge. + */ +public class LPForgePlugin extends MinecraftLuckPermsPlugin { + private ForgeSenderFactory senderFactory; + private ForgeConnectionListener connectionListener; + private ForgeCommandExecutor commandManager; + private ForgeContextManager contextManager; + + public LPForgePlugin(LPForgeBootstrap bootstrap) { + super(bootstrap); + } + + protected void registerEarlyListeners() { + this.connectionListener = new ForgeConnectionListener(this); + this.bootstrap.registerListeners(this.connectionListener); + + ForgePlatformListener platformListener = new ForgePlatformListener(this); + this.bootstrap.registerListeners(platformListener); + + ForgePermissionHandlerListener permissionHandlerListener = new ForgePermissionHandlerListener(this); + this.bootstrap.registerListeners(permissionHandlerListener); + + if (!skipCommandRegistration()) { + this.commandManager = new ForgeCommandExecutor(this); + this.bootstrap.registerListeners(this.commandManager); + } + + PluginMessageMessenger.registerChannel(); + } + + @Override + protected void setupSenderFactory() { + this.senderFactory = new ForgeSenderFactory(this); + } + + @Override + protected Set getGlobalDependencies() { + Set dependencies = super.getGlobalDependencies(); + dependencies.add(Dependency.CONFIGURATE_CORE); + dependencies.add(Dependency.CONFIGURATE_HOCON); + dependencies.add(Dependency.HOCON_CONFIG); + return dependencies; + } + + @Override + protected ConfigurationAdapter provideConfigurationAdapter() { + return new ForgeConfigAdapter(this, resolveConfig("luckperms.conf")); + } + + @Override + protected void registerPlatformListeners() { + // Too late for Forge, registered in #registerEarlyListeners + } + + @Override + protected MessagingFactory provideMessagingFactory() { + return new ForgeMessagingFactory(this); + } + + @Override + protected void registerCommands() { + // Too late for Forge, registered in #registerEarlyListeners + } + + @Override + protected void setupContextManager() { + this.contextManager = new ForgeContextManager(this); + + ForgePlayerCalculator playerCalculator = new ForgePlayerCalculator(this, getConfiguration().get(ConfigKeys.DISABLED_CONTEXTS)); + this.bootstrap.registerListeners(playerCalculator); + this.contextManager.registerCalculator(playerCalculator); + } + + @Override + protected void setupPlatformHooks() { + } + + @Override + protected AbstractEventBus provideEventBus(LuckPermsApiProvider provider) { + return new ForgeEventBus(this, provider); + } + + @Override + protected void registerApiOnPlatform(LuckPerms api) { + } + + @Override + protected void performFinalSetup() { + // register autoop listener + if (getConfiguration().get(ConfigKeys.AUTO_OP)) { + getApiProvider().getEventBus().subscribe(new MinecraftAutoOpListener(this)); + } + + // register forge command list updater + if (getConfiguration().get(ConfigKeys.UPDATE_CLIENT_COMMAND_LIST)) { + getApiProvider().getEventBus().subscribe(new MinecraftCommandListUpdater(this)); + } + } + + public ForgeSenderFactory getSenderFactory() { + return this.senderFactory; + } + + @Override + public ForgeConnectionListener getConnectionListener() { + return this.connectionListener; + } + + @Override + public ForgeCommandExecutor getCommandManager() { + return this.commandManager; + } + + @Override + public ForgeContextManager getContextManager() { + return this.contextManager; + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/context/ForgeContextManager.java b/forge/src/main/java/me/lucko/luckperms/forge/context/ForgeContextManager.java new file mode 100644 index 000000000..e0e6bb8cc --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/context/ForgeContextManager.java @@ -0,0 +1,53 @@ +/* + * 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.forge.context; + +import me.lucko.luckperms.common.context.manager.SimpleContextManager; +import me.lucko.luckperms.common.minecraft.context.MinecraftContextManager; +import me.lucko.luckperms.forge.LPForgePlugin; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.level.ServerPlayer; + +import java.util.UUID; + +public class ForgeContextManager extends SimpleContextManager implements MinecraftContextManager { + public ForgeContextManager(LPForgePlugin plugin) { + super(plugin, ServerPlayer.class, ServerPlayer.class); + } + + @Override + public UUID getUniqueId(ServerPlayer player) { + return player.getUUID(); + } + + @Override + public void customizeQueryOptions(ServerPlayer subject, QueryOptions.Builder builder) { + if (subject.level().getServer().isSingleplayerOwner(subject.nameAndId())) { + builder.option(INTEGRATED_SERVER_OWNER, true); + } + } + +} 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 new file mode 100644 index 000000000..54b281b80 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/context/ForgePlayerCalculator.java @@ -0,0 +1,55 @@ +/* + * 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.forge.context; + +import me.lucko.luckperms.common.minecraft.context.MinecraftPlayerCalculator; +import me.lucko.luckperms.forge.LPForgePlugin; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.listener.SubscribeEvent; + +import java.util.Set; + +public class ForgePlayerCalculator extends MinecraftPlayerCalculator { + public ForgePlayerCalculator(LPForgePlugin plugin, Set disabled) { + super(plugin, disabled); + } + + @SubscribeEvent + public void onPlayerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) { + if (this.world || this.dimensionType) { + this.plugin.getContextManager().signalContextUpdate((ServerPlayer) event.getEntity()); + } + } + + @SubscribeEvent + public void onPlayerChangeGameMode(PlayerEvent.PlayerChangeGameModeEvent event) { + if (this.gamemode) { + this.plugin.getContextManager().signalContextUpdate((ServerPlayer) event.getEntity()); + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeConnectionListener.java b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeConnectionListener.java new file mode 100644 index 000000000..ade7cda7b --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgeConnectionListener.java @@ -0,0 +1,157 @@ +/* + * 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.forge.listeners; + +import com.mojang.authlib.GameProfile; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; +import me.lucko.luckperms.forge.ForgeSenderFactory; +import me.lucko.luckperms.forge.LPForgePlugin; +import me.lucko.luckperms.forge.util.AsyncConfigurationTask; +import net.kyori.adventure.text.Component; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketListener; +import net.minecraft.network.protocol.login.ClientboundLoginDisconnectPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ConfigurationTask; +import net.minecraft.server.network.ServerConfigurationPacketListenerImpl; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedInEvent; +import net.minecraftforge.event.network.GatherLoginConfigurationTasksEvent; +import net.minecraftforge.eventbus.api.listener.Priority; +import net.minecraftforge.eventbus.api.listener.SubscribeEvent; + +import java.util.UUID; + +public class ForgeConnectionListener extends AbstractConnectionListener { + private static final ConfigurationTask.Type USER_LOGIN_TASK_TYPE = new ConfigurationTask.Type("luckperms:user_login"); + + private final LPForgePlugin plugin; + + public ForgeConnectionListener(LPForgePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + @SubscribeEvent + public void onGatherLoginConfigurationTasks(GatherLoginConfigurationTasksEvent event) { + PacketListener packetListener = event.getConnection().getPacketListener(); + if (!(packetListener instanceof ServerConfigurationPacketListenerImpl)) { + return; + } + + GameProfile gameProfile = ((ServerConfigurationPacketListenerImpl) packetListener).getOwner(); + if (gameProfile == null) { + return; + } + + String username = gameProfile.name(); + UUID uniqueId = gameProfile.id(); + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login (sync phase) for " + uniqueId + " - " + username); + } + + AsyncConfigurationTask task = new AsyncConfigurationTask( + this.plugin, + USER_LOGIN_TASK_TYPE, + () -> onPlayerNegotiationAsync(event.getConnection(), uniqueId, username) + ); + event.addTask(task); + } + + private void onPlayerNegotiationAsync(Connection connection, UUID uniqueId, String username) { + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login (async phase) for " + uniqueId + " - " + username); + } + + /* Actually process the login for the connection. + We do this here to delay the login until the data is ready. + If the login gets cancelled later on, then this will be cleaned up. + + This includes: + - loading uuid data + - loading permissions + - creating a user instance in the UserManager for this connection. + - setting up cached data. */ + try { + User user = loadUser(uniqueId, username); + recordConnection(uniqueId); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(uniqueId, username, user); + } catch (Exception ex) { + this.plugin.getLogger().severe("Exception occurred whilst loading data for " + uniqueId + " - " + username, ex); + + if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { + Component component = TranslationManager.render(Message.LOADING_DATABASE_ERROR.build()); + connection.send(new ClientboundLoginDisconnectPacket(ForgeSenderFactory.toNativeText(component))); + connection.disconnect(ForgeSenderFactory.toNativeText(component)); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(uniqueId, username, null); + } + } + } + + @SubscribeEvent(priority = Priority.HIGHEST) + public void onPlayerLoggedIn(PlayerLoggedInEvent event) { + ServerPlayer player = (ServerPlayer) event.getEntity(); + GameProfile profile = player.getGameProfile(); + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing post-login for " + profile.id() + " - " + profile.name()); + } + + User user = this.plugin.getUserManager().getIfLoaded(profile.id()); + + if (user == null) { + if (!getUniqueConnections().contains(profile.id())) { + this.plugin.getLogger().warn("User " + profile.id() + " - " + profile.name() + + " doesn't have data pre-loaded, they have never been processed during pre-login in this session."); + } else { + this.plugin.getLogger().warn("User " + profile.id() + " - " + profile.name() + + " doesn't currently have data pre-loaded, but they have been processed before in this session."); + } + + Component component = TranslationManager.render(Message.LOADING_STATE_ERROR.build(), player.getLanguage()); + if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { + player.connection.disconnect(ForgeSenderFactory.toNativeText(component)); + return; + } else { + player.sendSystemMessage(ForgeSenderFactory.toNativeText(component)); + } + } + + this.plugin.getContextManager().signalContextUpdate(player); + } + + @SubscribeEvent(priority = Priority.LOWEST) + public void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) { + ServerPlayer player = (ServerPlayer) event.getEntity(); + handleDisconnect(player.getGameProfile().id()); + } + +} \ No newline at end of file diff --git a/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgePlatformListener.java b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgePlatformListener.java new file mode 100644 index 000000000..ab05bf398 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/listeners/ForgePlatformListener.java @@ -0,0 +1,93 @@ +/* + * 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.forge.listeners; + +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.minecraft.command.BrigadierInjector; +import me.lucko.luckperms.forge.LPForgePlugin; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.server.players.ServerOpList; +import net.minecraftforge.event.AddReloadListenerEvent; +import net.minecraftforge.event.CommandEvent; +import net.minecraftforge.event.server.ServerStartedEvent; +import net.minecraftforge.eventbus.api.listener.SubscribeEvent; + +import java.io.IOException; +import java.util.Locale; + +public class ForgePlatformListener { + private final LPForgePlugin plugin; + + public ForgePlatformListener(LPForgePlugin plugin) { + this.plugin = plugin; + } + + @SubscribeEvent + public boolean onCommand(CommandEvent event) { + CommandContextBuilder context = event.getParseResults().getContext(); + + if (!this.plugin.getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + for (ParsedCommandNode node : context.getNodes()) { + if (!(node.getNode() instanceof LiteralCommandNode)) { + continue; + } + + String name = node.getNode().getName().toLowerCase(Locale.ROOT); + if (name.equals("op") || name.equals("deop")) { + Message.OP_DISABLED.send(this.plugin.getSenderFactory().wrap(context.getSource())); + return true; // cancel + } + } + } + + return false; // don't cancel + } + + @SubscribeEvent + public void onAddReloadListener(AddReloadListenerEvent event) { + Commands commands = event.getServerResources().getCommands(); + BrigadierInjector.inject(this.plugin, commands.getDispatcher()); + } + + @SubscribeEvent + public void onServerStarted(ServerStartedEvent event) { + if (!this.plugin.getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + ServerOpList ops = event.getServer().getPlayerList().getOps(); + ops.getEntries().clear(); + try { + ops.save(); + } catch (IOException ex) { + this.plugin.getLogger().severe("Encountered an error while saving ops", ex); + } + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/messaging/ForgeMessagingFactory.java b/forge/src/main/java/me/lucko/luckperms/forge/messaging/ForgeMessagingFactory.java new file mode 100644 index 000000000..26852ba64 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/messaging/ForgeMessagingFactory.java @@ -0,0 +1,70 @@ +/* + * 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.forge.messaging; + +import me.lucko.luckperms.common.messaging.InternalMessagingService; +import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.forge.LPForgePlugin; +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.luckperms.api.messenger.MessengerProvider; +import org.checkerframework.checker.nullness.qual.NonNull; + +public class ForgeMessagingFactory extends MessagingFactory { + public ForgeMessagingFactory(LPForgePlugin plugin) { + super(plugin); + } + + @Override + protected InternalMessagingService getServiceFor(String messagingType) { + if (messagingType.equals("pluginmsg") || messagingType.equals("bungee") || messagingType.equals("velocity")) { + try { + return new LuckPermsMessagingService(getPlugin(), new PluginMessageMessengerProvider()); + } catch (Exception e) { + getPlugin().getLogger().severe("Exception occurred whilst enabling messaging", e); + } + } + + return super.getServiceFor(messagingType); + } + + private class PluginMessageMessengerProvider implements MessengerProvider { + + @Override + public @NonNull String getName() { + return "PluginMessage"; + } + + @Override + public @NonNull Messenger obtain(@NonNull IncomingMessageConsumer incomingMessageConsumer) { + PluginMessageMessenger messenger = new PluginMessageMessenger(getPlugin(), incomingMessageConsumer); + messenger.init(); + return messenger; + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/messaging/PluginMessageMessenger.java b/forge/src/main/java/me/lucko/luckperms/forge/messaging/PluginMessageMessenger.java new file mode 100644 index 000000000..fe022e887 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/messaging/PluginMessageMessenger.java @@ -0,0 +1,101 @@ +/* + * 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.forge.messaging; + +import com.google.common.collect.Iterables; +import io.netty.buffer.Unpooled; +import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; +import me.lucko.luckperms.forge.LPForgePlugin; +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraftforge.network.ChannelBuilder; +import net.minecraftforge.network.EventNetworkChannel; +import net.minecraftforge.network.PacketDistributor; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public class PluginMessageMessenger extends AbstractPluginMessageMessenger implements Messenger { + private static final Identifier CHANNEL_ID = Identifier.parse(AbstractPluginMessageMessenger.CHANNEL); + private static final EventNetworkChannel CHANNEL = ChannelBuilder.named(CHANNEL_ID).optional().eventNetworkChannel(); + + private final LPForgePlugin plugin; + + public PluginMessageMessenger(LPForgePlugin plugin, IncomingMessageConsumer consumer) { + super(consumer); + this.plugin = plugin; + } + + public void init() { + CHANNEL.addListener(event -> { + byte[] buf = new byte[event.getPayload().readableBytes()]; + event.getPayload().readBytes(buf); + + handleIncomingMessage(buf); + event.getSource().setPacketHandled(true); + }); + } + + @Override + protected void sendOutgoingMessage(byte[] buf) { + AtomicReference taskRef = new AtomicReference<>(); + SchedulerTask task = this.plugin.getBootstrap().getScheduler().asyncRepeating(() -> { + ServerPlayer player = this.plugin.getBootstrap().getServer() + .map(MinecraftServer::getPlayerList) + .map(PlayerList::getPlayers) + .map(players -> Iterables.getFirst(players, null)) + .orElse(null); + + if (player == null) { + return; + } + + FriendlyByteBuf byteBuf = new FriendlyByteBuf(Unpooled.buffer()); + byteBuf.writeBytes(buf); + + CHANNEL.send(byteBuf, PacketDistributor.PLAYER.with(player)); + + SchedulerTask t = taskRef.getAndSet(null); + if (t != null) { + t.cancel(); + } + }, 10, TimeUnit.SECONDS); + taskRef.set(task); + } + + @SuppressWarnings("EmptyMethod") + public static void registerChannel() { + // do nothing - the channels are registered in the static initializer, we just + // need to make sure that is called (which it will be if this method runs) + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandler.java b/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandler.java new file mode 100644 index 000000000..ddd30d5d9 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandler.java @@ -0,0 +1,162 @@ +/* + * 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.forge.service; + +import me.lucko.luckperms.common.cacheddata.type.MetaCache; +import me.lucko.luckperms.common.cacheddata.type.PermissionCache; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.forge.LPForgeBootstrap; +import me.lucko.luckperms.forge.LPForgePlugin; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.query.QueryMode; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.server.permission.handler.IPermissionHandler; +import net.minecraftforge.server.permission.nodes.PermissionDynamicContext; +import net.minecraftforge.server.permission.nodes.PermissionNode; +import net.minecraftforge.server.permission.nodes.PermissionType; +import net.minecraftforge.server.permission.nodes.PermissionTypes; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class ForgePermissionHandler implements IPermissionHandler { + public static final Identifier IDENTIFIER = Identifier.fromNamespaceAndPath(LPForgeBootstrap.ID, "permission_handler"); + + private final LPForgePlugin plugin; + private final Set> permissionNodes; + + public ForgePermissionHandler(LPForgePlugin plugin, Collection> permissionNodes) { + this.plugin = plugin; + this.permissionNodes = Collections.unmodifiableSet(new HashSet<>(permissionNodes)); + + for (PermissionNode node : this.permissionNodes) { + this.plugin.getPermissionRegistry().insert(node.getNodeName()); + } + } + + @Override + public Identifier getIdentifier() { + return IDENTIFIER; + } + + @Override + public Set> getRegisteredNodes() { + return this.permissionNodes; + } + + @Override + public T getPermission(ServerPlayer player, PermissionNode node, PermissionDynamicContext... context) { + User user = this.plugin.getUserManager().getIfLoaded(player.getUUID()); + if (user != null) { + QueryOptions queryOptions = this.plugin.getContextManager().getQueryOptions(player); + T value = getPermissionValue(user, queryOptions, node, context); + if (value != null) { + return value; + } + } + + return node.getDefaultResolver().resolve(player, player.getUUID(), context); + } + + @Override + public T getOfflinePermission(UUID player, PermissionNode node, PermissionDynamicContext... context) { + User user = this.plugin.getUserManager().getIfLoaded(player); + + if (user != null) { + QueryOptions queryOptions = user.getQueryOptions(); + T value = getPermissionValue(user, queryOptions, node, context); + if (value != null) { + return value; + } + } + + return node.getDefaultResolver().resolve(null, player, context); + } + + @SuppressWarnings("unchecked") + private static T getPermissionValue(User user, QueryOptions queryOptions, PermissionNode node, PermissionDynamicContext... context) { + queryOptions = appendContextToQueryOptions(queryOptions, context); + String key = node.getNodeName(); + PermissionType type = node.getType(); + + // permission check + if (type == PermissionTypes.BOOLEAN) { + PermissionCache cache = user.getCachedData().getPermissionData(queryOptions); + Tristate value = cache.checkPermission(key, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + if (value != Tristate.UNDEFINED) { + return (T) (Boolean) value.asBoolean(); + } + } + + // meta lookup + if (node.getType() == PermissionTypes.STRING) { + MetaCache cache = user.getCachedData().getMetaData(queryOptions); + String value = cache.getMetaOrChatMetaValue(node.getNodeName(), CheckOrigin.PLATFORM_API); + if (value != null) { + return (T) value; + } + } + + // meta lookup (integer) + if (node.getType() == PermissionTypes.INTEGER) { + MetaCache cache = user.getCachedData().getMetaData(queryOptions); + String value = cache.getMetaOrChatMetaValue(node.getNodeName(), CheckOrigin.PLATFORM_API); + if (value != null) { + try { + return (T) Integer.valueOf(Integer.parseInt(value)); + } catch (IllegalArgumentException e) { + // ignore + } + } + } + + return null; + } + + private static QueryOptions appendContextToQueryOptions(QueryOptions queryOptions, PermissionDynamicContext... context) { + if (context.length == 0 || queryOptions.mode() != QueryMode.CONTEXTUAL) { + return queryOptions; + } + + ImmutableContextSet.Builder contextBuilder = new ImmutableContextSetImpl.BuilderImpl() + .addAll(queryOptions.context()); + + for (PermissionDynamicContext dynamicContext : context) { + contextBuilder.add(dynamicContext.getDynamic().name(), dynamicContext.getSerializedValue()); + } + + return queryOptions.toBuilder().context(contextBuilder.build()).build(); + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandlerListener.java b/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandlerListener.java new file mode 100644 index 000000000..c2b9e6231 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/service/ForgePermissionHandlerListener.java @@ -0,0 +1,64 @@ +/* + * 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.forge.service; + +import me.lucko.luckperms.common.command.access.CommandPermission; +import me.lucko.luckperms.forge.LPForgePlugin; +import net.minecraftforge.common.ForgeConfig; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.eventbus.api.listener.SubscribeEvent; +import net.minecraftforge.server.permission.events.PermissionGatherEvent; +import net.minecraftforge.server.permission.handler.DefaultPermissionHandler; +import net.minecraftforge.server.permission.nodes.PermissionNode; +import net.minecraftforge.server.permission.nodes.PermissionTypes; + +public class ForgePermissionHandlerListener { + private final LPForgePlugin plugin; + + public ForgePermissionHandlerListener(LPForgePlugin plugin) { + this.plugin = plugin; + } + + @SubscribeEvent + public void onPermissionGatherHandler(PermissionGatherEvent.Handler event) { + // Override the default permission handler with LuckPerms + ForgeConfigSpec.ConfigValue permissionHandler = ForgeConfig.SERVER.permissionHandler; + if (permissionHandler.get().equals(DefaultPermissionHandler.IDENTIFIER.toString())) { + permissionHandler.set(ForgePermissionHandler.IDENTIFIER.toString()); + } + + event.addPermissionHandler(ForgePermissionHandler.IDENTIFIER, permissions -> new ForgePermissionHandler(this.plugin, permissions)); + } + + @SubscribeEvent + public void onPermissionGatherNodes(PermissionGatherEvent.Nodes event) { + // register luckperms nodes + for (CommandPermission permission : CommandPermission.values()) { + event.addNodes(new PermissionNode<>("luckperms", permission.getNode(), PermissionTypes.BOOLEAN, (player, uuid, context) -> false)); + } + } + +} diff --git a/forge/src/main/java/me/lucko/luckperms/forge/util/AsyncConfigurationTask.java b/forge/src/main/java/me/lucko/luckperms/forge/util/AsyncConfigurationTask.java new file mode 100644 index 000000000..f873f07b9 --- /dev/null +++ b/forge/src/main/java/me/lucko/luckperms/forge/util/AsyncConfigurationTask.java @@ -0,0 +1,49 @@ +/* + * 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.forge.util; + +import me.lucko.luckperms.common.minecraft.util.AbstractAsyncConfigurationTask; +import me.lucko.luckperms.forge.LPForgePlugin; +import net.minecraft.network.protocol.Packet; +import net.minecraftforge.network.config.ConfigurationTaskContext; + +import java.util.function.Consumer; + +public class AsyncConfigurationTask extends AbstractAsyncConfigurationTask { + public AsyncConfigurationTask(LPForgePlugin plugin, Type type, Runnable task) { + super(plugin, type, task); + } + + @Override + public void start(ConfigurationTaskContext ctx) { + start(() -> ctx.finish(type())); + } + + @Override + public void start(Consumer> send) { + throw new IllegalStateException("This should never be called"); + } +} \ No newline at end of file diff --git a/forge/src/main/resources/luckperms.conf b/forge/src/main/resources/luckperms.conf new file mode 100644 index 000000000..9a5550225 --- /dev/null +++ b/forge/src/main/resources/luckperms.conf @@ -0,0 +1,693 @@ +#################################################################################################### +# +----------------------------------------------------------------------------------------------+ # +# | __ __ ___ __ __ | # +# | | | | / ` |__/ |__) |__ |__) |\/| /__` | # +# | |___ \__/ \__, | \ | |___ | \ | | .__/ | # +# | | # +# | https://luckperms.net | # +# | | # +# | WIKI: https://luckperms.net/wiki | # +# | DISCORD: https://discord.gg/luckperms | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # +# | | # +# | Each option in this file is documented and explained here: | # +# | ==> https://luckperms.net/wiki/Configuration | # +# | | # +# | New options are not added to this file automatically. Default values are used if an | # +# | option cannot be found. The latest config versions can be obtained at the link above. | # +# +----------------------------------------------------------------------------------------------+ # +#################################################################################################### + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | ESSENTIAL SETTINGS | # +# | | # +# | Important settings that control how LuckPerms functions. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The name of the server, used for server specific permissions. +# +# - When set to "global" this setting is effectively ignored. +# - In all other cases, the value here is added to all players in a "server" context. +# - See: https://luckperms.net/wiki/Context +server = "global" + +# If the servers own UUID cache/lookup facility should be used when there is no record for a player +# already in LuckPerms. +# +# - When this is set to 'false', commands using a player's username will not work unless the player +# has joined since LuckPerms was first installed. +# - To get around this, you can use a player's uuid directly in the command, or enable this option. +# - When this is set to 'true', the server facility is used. This may use a number of methods, +# including checking the servers local cache, or making a request to the Mojang API. +use-server-uuid-cache = false + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | STORAGE SETTINGS | # +# | | # +# | Controls which storage method LuckPerms will use to store data. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# How the plugin should store data +# +# - The various options are explained in more detail on the wiki: +# https://luckperms.net/wiki/Storage-types +# +# - Possible options: +# +# | Remote databases - require connection information to be configured below +# |=> MySQL +# |=> MariaDB (preferred over MySQL) +# |=> PostgreSQL +# |=> MongoDB +# +# | Flatfile/local database - don't require any extra configuration +# |=> H2 (preferred over SQLite) +# |=> SQLite +# +# | Readable & editable text files - don't require any extra configuration +# |=> YAML (.yml files) +# |=> JSON (.json files) +# |=> HOCON (.conf files) +# |=> TOML (.toml files) +# | +# | By default, user, group and track data is separated into different files. Data can be combined +# | and all stored in the same file by switching to a combined storage variant. +# | Just add '-combined' to the end of the storage-method, e.g. 'yaml-combined' +# +# - A H2 database is the default option. +# - If you want to edit data manually in "traditional" storage files, we suggest using YAML. +storage-method = "h2" + +# The following block defines the settings for remote database storage methods. +# +# - You don't need to touch any of the settings here if you're using a local storage method! +# - The connection detail options are shared between all remote storage types. +data { + + # Define the address and port for the database. + # - The standard DB engine port is used by default + # (MySQL = 3306, PostgreSQL = 5432, MongoDB = 27017) + # - Specify as "host:port" if differs + address = "localhost" + + # The name of the database to store LuckPerms data in. + # - This must be created already. Don't worry about this setting if you're using MongoDB. + database = "minecraft" + + # Credentials for the database. + username = "root" + password = "" + + # These settings apply to the MySQL connection pool. + # - The default values will be suitable for the majority of users. + # - Do not change these settings unless you know what you're doing! + pool-settings { + + # Sets the maximum size of the MySQL connection pool. + # - Basically this value will determine the maximum number of actual + # connections to the database backend. + # - More information about determining the size of connection pools can be found here: + # https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + maximum-pool-size = 10 + + # Sets the minimum number of idle connections that the pool will try to maintain. + # - For maximum performance and responsiveness to spike demands, it is recommended to not set + # this value and instead allow the pool to act as a fixed size connection pool. + # (set this value to the same as 'maximum-pool-size') + minimum-idle = 10 + + # This setting controls the maximum lifetime of a connection in the pool in milliseconds. + # - The value should be at least 30 seconds less than any database or infrastructure imposed + # connection time limit. + maximum-lifetime = 1800000 # 30 minutes + + # This setting controls how frequently the pool will 'ping' a connection in order to prevent it + # from being timed out by the database or network infrastructure, measured in milliseconds. + # - The value should be less than maximum-lifetime and greater than 30000 (30 seconds). + # - Setting the value to zero will disable the keepalive functionality. + keepalive-time = 0 + + # This setting controls the maximum number of milliseconds that the plugin will wait for a + # connection from the pool, before timing out. + connection-timeout = 5000 # 5 seconds + + # This setting allows you to define extra properties for connections. + # + # By default, the following options are set to enable utf8 encoding. (you may need to remove + # these if you are using PostgreSQL) + # useUnicode = true + # characterEncoding = "utf8" + # + # You can also use this section to disable SSL connections, by uncommenting the 'useSSL' and + # 'verifyServerCertificate' options below. + properties { + useUnicode = true + characterEncoding = "utf8" + #useSSL: false + #verifyServerCertificate: false + } + } + + # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). + # - Change this if you want to use different tables for different servers. + table-prefix = "luckperms_" + + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. + mongodb-collection-prefix = "" + + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ + mongodb-connection-uri = "" +} + +# Define settings for a "split" storage setup. +# +# - This allows you to define a storage method for each type of data. +# - The connection options above still have to be correct for each type here. +split-storage { + # Don't touch this if you don't want to use split storage! + enabled = false + methods { + # These options don't need to be modified if split storage isn't enabled. + user = "h2" + group = "h2" + track = "h2" + uuid = "h2" + log = "h2" + } +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | UPDATE PROPAGATION & MESSAGING SERVICE | # +# | | # +# | Controls the ways in which LuckPerms will sync data & notify other servers of changes. | # +# | These options are documented on greater detail on the wiki under "Instant Updates". | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# This option controls how frequently LuckPerms will perform a sync task. +# +# - A sync task will refresh all data from the storage, and ensure that the most up-to-date data is +# being used by the plugin. +# - This is disabled by default, as most users will not need it. However, if you're using a remote +# storage type without a messaging service setup, you may wish to set this to something like 3. +# - Set to -1 to disable the task completely. +sync-minutes = -1 + +# If the file watcher should be enabled. +# +# - When using a file-based storage type, LuckPerms can monitor the data files for changes, and +# automatically update when changes are detected. +# - If you don't want this feature to be active, set this option to false. +watch-files = true + +# Define which messaging service should be used by the plugin. +# +# - If enabled and configured, LuckPerms will use the messaging service to inform other connected +# servers of changes. +# - Use the command "/lp networksync" to manually push changes. +# - Data is NOT stored using this service. It is only used as a messaging platform. +# +# - If you decide to enable this feature, you should set "sync-minutes" to -1, as there is no need +# for LuckPerms to poll the database for changes. +# +# - Possible options: +# => sql Uses the SQL database to form a queue system for communication. Will only work when +# 'storage-method' is set to MySQL or MariaDB. This is chosen by default if the +# option is set to 'auto' and SQL storage is in use. Set to 'notsql' to disable this. +# => pluginmsg Uses the plugin messaging channels to communicate with the proxy. +# LuckPerms must be installed on your proxy & all connected servers backend servers. +# Won't work if you have more than one proxy. +# => redis Uses Redis pub-sub to push changes. Your server connection info must be configured +# below. +# => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. +# => auto Attempts to automatically setup a messaging service using redis or sql. +messaging-service = "auto" + +# If LuckPerms should automatically push updates after a change has been made with a command. +auto-push-updates = true + +# If LuckPerms should push logging entries to connected servers via the messaging service. +push-log-entries = true + +# If LuckPerms should broadcast received logging entries to players on this platform. +# +# - If you have LuckPerms installed on your backend servers as well as a BungeeCord proxy, you +# should set this option to false on either your backends or your proxies, to avoid players being +# messaged twice about log entries. +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" + username = "" + password = "" + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel { + enabled = false + master = "mymaster" + addresses = ["localhost:26379"] + username = "" + password = "" + } +} + +# Settings for nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats { + enabled = false + address = "localhost" + username = "" + password = "" + token = "" +} + +# Settings for RabbitMQ. +# Port 5672 is used by default; set address to "host:port" if differs +rabbitmq { + enabled = false + address = "localhost" + vhost = "/" + username = "guest" + password = "guest" +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | CUSTOMIZATION SETTINGS | # +# | | # +# | Settings that allow admins to customize the way LuckPerms operates. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls how temporary permissions/parents/meta should be accumulated. +# +# - The default behaviour is "deny". +# - This behaviour can also be specified when the command is executed. See the command usage +# documentation for more info. +# +# - Possible options: +# => accumulate durations will be added to the existing expiry time +# => replace durations will be replaced if the new duration is later than the current +# expiration +# => deny the command will just fail if you try to add another node with the same expiry +temporary-add-behaviour = "deny" + +# Controls how LuckPerms will determine a users "primary" group. +# +# - The meaning and influence of "primary groups" are explained in detail on the wiki. +# - The preferred approach is to let LuckPerms automatically determine a users primary group +# based on the relative weight of their parent groups. +# +# - Possible options: +# => stored use the value stored against the users record in the file/database +# => parents-by-weight just use the users most highly weighted parent +# => all-parents-by-weight same as above, but calculates based upon all parents inherited from +# both directly and indirectly +primary-group-calculation = "parents-by-weight" + +# If the plugin should check for "extra" permissions with users run LP commands. +# +# - These extra permissions allow finer control over what users can do with each command, and who +# they have access to edit. +# - The nature of the checks are documented on the wiki under "Argument based command permissions". +# - Argument based permissions are *not* static, unlike the 'base' permissions, and will depend upon +# the arguments given within the command. +argument-based-command-permissions = false + +# If the plugin should check whether senders are a member of a given group before they're able to +# edit the groups data or add/remove other users to/from it. +# Note: these limitations do not apply to the web editor! +require-sender-group-membership-to-modify = false + +# If the plugin should send log notifications to users whenever permissions are modified. +# +# - Notifications are only sent to those with the appropriate permission to receive them +# - They can also be temporarily enabled/disabled on a per-user basis using +# '/lp log notify ' +log-notify = true + +# Defines a list of log entries which should not be sent as notifications to users. +# +# - Each entry in the list is a RegEx expression which is matched against the log entry description. +log-notify-filtered-descriptions = [ +# "parent add example" +] + +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations = true + +# Defines the options for prefix and suffix stacking. +# +# - The feature allows you to display multiple prefixes or suffixes alongside a players username in +# chat. +# - It is explained and documented in more detail on the wiki under "Prefix & Suffix Stacking". +# +# - The options are divided into separate sections for prefixes and suffixes. +# - The 'duplicates' setting refers to how duplicate elements are handled. Can be 'retain-all', +# 'first-only' or 'last-only'. +# - The value of 'start-spacer' is included at the start of the resultant prefix/suffix. +# - The value of 'end-spacer' is included at the end of the resultant prefix/suffix. +# - The value of 'middle-spacer' is included between each element in the resultant prefix/suffix. +# +# - Possible format options: +# => highest Selects the value with the highest weight, from all values +# held by or inherited by the player. +# +# => lowest Same as above, except takes the one with the lowest weight. +# +# => highest_own Selects the value with the highest weight, but will not +# accept any inherited values. +# +# => lowest_own Same as above, except takes the value with the lowest weight. +# +# => highest_inherited Selects the value with the highest weight, but will only +# accept inherited values. +# +# => lowest_inherited Same as above, except takes the value with the lowest weight. +# +# => highest_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group on the given track. +# +# => lowest_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group not on the given track. +# +# => lowest_not_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_from_group_ Selects the value with the highest weight, but only if the +# value was inherited from the given group. +# +# => lowest_from_group_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_from_group_ Selects the value with the highest weight, but only if the +# value was not inherited from the given group. +# +# => lowest_not_from_group_ Same as above, except takes the value with the lowest weight. +meta-formatting { + prefix { + format = [ + "highest" + ] + duplicates = "first-only" + start-spacer = "" + middle-spacer = " " + end-spacer = "" + } + suffix { + format = [ + "highest" + ] + duplicates = "first-only" + start-spacer = "" + middle-spacer = " " + end-spacer = "" + } +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | PERMISSION CALCULATION AND INHERITANCE | # +# | | # +# | Modify the way permission checks, meta lookups and inheritance resolutions are handled. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The algorithm LuckPerms should use when traversing the "inheritance tree". +# +# - Possible options: +# => breadth-first See: https://en.wikipedia.org/wiki/Breadth-first_search +# => depth-first-pre-order See: https://en.wikipedia.org/wiki/Depth-first_search +# => depth-first-post-order See: https://en.wikipedia.org/wiki/Depth-first_search +inheritance-traversal-algorithm = "depth-first-pre-order" + +# If a final sort according to "inheritance rules" should be performed after the traversal algorithm +# has resolved the inheritance tree. +# +# "Inheritance rules" refers to things such as group weightings, primary group status, and the +# natural contextual ordering of the group nodes. +# +# Setting this to 'true' will allow for the inheritance rules to take priority over the structure of +# the inheritance tree. +# +# Effectively when this setting is 'true': the tree is flattened, and rules applied afterwards, +# and when this setting is 'false':, the rules are just applied during each step of the traversal. +post-traversal-inheritance-sort = false + +# Defines the mode used to determine whether a set of contexts are satisfied. +# +# - Possible options: +# => at-least-one-value-per-key Set A will be satisfied by another set B, if at least one of the +# key-value entries per key in A are also in B. +# => all-values-per-key Set A will be satisfied by another set B, if all key-value +# entries in A are also in B. +context-satisfy-mode = "at-least-one-value-per-key" + +# LuckPerms has a number of built-in contexts. These can be disabled by adding the context key to +# the list below. +disabled-contexts = [ +# "world" +] + +# +----------------------------------------------------------------------------------------------+ # +# | Permission resolution settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If users on this server should have their global permissions applied. +# When set to false, only server specific permissions will apply for users on this server +include-global = true + +# If users on this server should have their global world permissions applied. +# When set to false, only world specific permissions will apply for users on this server +include-global-world = true + +# If users on this server should have global (non-server specific) groups applied +apply-global-groups = true + +# If users on this server should have global (non-world specific) groups applied +apply-global-world-groups = true + +# +----------------------------------------------------------------------------------------------+ # +# | Meta lookup settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Defines how meta values should be selected. +# +# - Possible options: +# => inheritance Selects the meta value that was inherited first +# => highest-number Selects the highest numerical meta value +# => lowest-number Selects the lowest numerical meta value +meta-value-selection-default = "inheritance" + +# Defines how meta values should be selected per key. +meta-value-selection { + #max-homes = "highest-number" +} + +# +----------------------------------------------------------------------------------------------+ # +# | Inheritance settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If the plugin should apply wildcard permissions. +# +# - If set to true, LuckPerms will detect wildcard permissions, and resolve & apply all registered +# permissions matching the wildcard. +apply-wildcards = true + +# If LuckPerms should resolve and apply permissions according to the Sponge style implicit wildcard +# inheritance system. +# +# - That being: If a user has been granted "example", then the player should have also be +# automatically granted "example.function", "example.another", "example.deeper.nesting", +# and so on. +apply-sponge-implicit-wildcards = false + +# If the plugin should parse regex permissions. +# +# - If set to true, LuckPerms will detect regex permissions, marked with "r=" at the start of the +# node, and resolve & apply all registered permissions matching the regex. +apply-regex = true + +# If the plugin should complete and apply shorthand permissions. +# +# - If set to true, LuckPerms will detect and expand shorthand node patterns. +apply-shorthand = true + +# If the owner of an integrated server should bypass permission checks. +# +# - This setting only applies when LuckPerms is active on a single-player world. +# - The owner of an integrated server is the player whose client instance is running the server. +integrated-server-owner-bypasses-checks = true + +# +----------------------------------------------------------------------------------------------+ # +# | Extra settings | # +# +----------------------------------------------------------------------------------------------+ # + +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators = [] + +# Allows you to set "aliases" for the worlds sent forward for context calculation. +# +# - These aliases are provided in addition to the real world name. Applied recursively. +# - Remove the comment characters for the default aliases to apply. +world-rewrite { + #world_nether = "world" + #world_the_end = "world" +} + +# Define special group weights for this server. +# +# - Group weights can also be applied directly to group data, using the setweight command. +# - This section allows weights to be set on a per-server basis. +group-weight { + #admin = 10 +} + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | FINE TUNING OPTIONS | # +# | | # +# | A number of more niche settings for tweaking and changing behaviour. The section also | # +# | contains toggles for some more specialised features. It is only necessary to make changes to | # +# | these options if you want to fine-tune LuckPerms behaviour. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# +----------------------------------------------------------------------------------------------+ # +# | Server Operator (OP) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls whether server operators should exist at all. +# +# - When set to 'false', all players will be de-opped, and the /op and /deop commands will be +# disabled. Note that vanilla features like the spawn-protection require an operator on the +# server to work. +enable-ops = true + +# Enables or disables a special permission based system in LuckPerms for controlling OP status. +# +# - If set to true, any user with the permission "luckperms.autoop" will automatically be granted +# server operator status. This permission can be inherited, or set on specific servers/worlds, +# temporarily, etc. +# - Additionally, setting this to true will force the "enable-ops" option above to false. All users +# will be de-opped unless they have the permission node, and the op/deop commands will be +# disabled. +# - It is recommended that you use this option instead of assigning a single '*' permission. +# - However, on Forge this setting can be used as a "pseudo" root wildcard, as many mods support +# the operator system over permissions. +auto-op = false + +# +----------------------------------------------------------------------------------------------+ # +# | Miscellaneous (and rarely used) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If LuckPerms should produce extra logging output when it handles logins. +# +# - Useful if you're having issues with UUID forwarding or data not being loaded. +debug-logins = false + +# If LuckPerms should allow usernames with non alphanumeric characters. +# +# - Note that due to the design of the storage implementation, usernames must still be 16 characters +# or less. +allow-invalid-usernames = false + +# If LuckPerms should not require users to confirm bulkupdate operations. +# +# - When set to true, operations will be executed immediately. +# - This is not recommended, as bulkupdate has the potential to irreversibly delete large amounts of +# data, and is not designed to be executed automatically. +# - If automation is needed, users should prefer using the LuckPerms API. +skip-bulkupdate-confirmation = false + +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate = false + +# If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. +# +# - When this happens, the plugin will set their primary group back to default. +prevent-primary-group-removal = false + +# If LuckPerms should update the list of commands sent to the client when permissions are changed. +update-client-command-list = true + +# If LuckPerms should attempt to resolve Vanilla command target selectors for LP commands. +# See here for more info: https://minecraft.wiki/w/Target_selectors +resolve-command-selectors = false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode { + players = false + console = false +} + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands { + players = false + console = false +} diff --git a/gradle.properties b/gradle.properties index f12c912d6..ab51df5b1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,4 @@ # Fabric requires some more ram. -org.gradle.jvmargs=-Xmx1G \ No newline at end of file +org.gradle.jvmargs=-Xmx2G +# ForgeGradle is special. +org.gradle.daemon=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..5787fdc34 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,15 @@ +[versions] +shadow = "8.3.8" +blossom = "2.2.0" +moddevgradle = "2.0.141" +forgegradle = "[7.0.13,8.0)" +loom = "1.15-SNAPSHOT" +licenser = "2.2.2" + +[plugins] +blossom = { id = "net.kyori.blossom", version.ref = "blossom" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } +moddevgradle = { id = "net.neoforged.moddev", version.ref = "moddevgradle" } +forgegradle = { id = "net.minecraftforge.gradle", version.ref = "forgegradle" } +loom = { id = "net.fabricmc.fabric-loom", version.ref = "loom" } +licenser = { id = "dev.yumi.gradle.licenser", version.ref = "licenser" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c02..d997cfc60 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2a563242c..c61a118f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c8..739907dfd 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,81 +15,114 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/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. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32c..c4bdd3ab8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,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,13 +43,13 @@ 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. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,32 +59,33 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%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/hytale/build.gradle b/hytale/build.gradle new file mode 100644 index 000000000..ff78609f3 --- /dev/null +++ b/hytale/build.gradle @@ -0,0 +1,60 @@ +plugins { + alias(libs.plugins.shadow) +} + +tasks.withType(JavaCompile).configureEach { + options.release = 21 +} + +dependencies { + implementation project(':common') + compileOnly project(':common:loader-utils') + + implementation 'com.google.guava:guava:19.0' + implementation 'com.google.code.gson:gson:2.7' + + compileOnly "com.hypixel.hytale:Server:${hytaleVersion}" +} + +repositories { + maven { url 'https://nexus.lucko.me/repository/maven-hytale/' } +} + +shadowJar { + archiveFileName = 'luckperms-hytale.jarinjar' + + dependencies { + include(dependency('me.lucko.luckperms:.*')) + include(dependency('com.google.guava:guava:.*')) + include(dependency('com.google.code.gson:gson:.*')) + } + + relocate('com.google.common', 'me.lucko.luckperms.lib.guava') { + exclude 'com.google.common.flogger.**' + } + relocate 'com.google.gson', 'me.lucko.luckperms.lib.gson' + + relocate 'net.kyori.adventure', 'me.lucko.luckperms.lib.adventure' + relocate 'net.kyori.event', 'me.lucko.luckperms.lib.eventbus' + relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine' + relocate 'okio', 'me.lucko.luckperms.lib.okio' + relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3' + relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy' + 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 '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' + relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' + relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' + relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' + relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' + relocate 'org.yaml.snakeyaml', 'me.lucko.luckperms.lib.yaml' +} + +artifacts { + archives shadowJar +} diff --git a/hytale/gradle.properties b/hytale/gradle.properties new file mode 100644 index 000000000..73e075d9e --- /dev/null +++ b/hytale/gradle.properties @@ -0,0 +1 @@ +hytaleVersion=0.5.2 diff --git a/hytale/loader-with-deps/build.gradle b/hytale/loader-with-deps/build.gradle new file mode 100644 index 000000000..f9b283488 --- /dev/null +++ b/hytale/loader-with-deps/build.gradle @@ -0,0 +1,149 @@ +plugins { + alias(libs.plugins.shadow) +} + +tasks.configureEach { + //enabled = false +} + +dependencies { + implementation project(':api') + implementation project(':common:loader-utils') + + compileOnly "com.hypixel.hytale:Server:${hytaleVersion}" +} + +repositories { + maven { url 'https://nexus.lucko.me/repository/maven-hytale/' } +} + +processResources { + filesMatching('manifest.json') { + expand ( + 'pluginVersion': project.ext.fullVersion, + 'hytaleVersion': hytaleVersion + ) + } +} + +shadowJar { + archiveFileName = "LuckPerms-Hytale-${project.ext.fullVersion}-with-deps.jar" + + from { + project(':hytale').tasks.shadowJar.archiveFile + } +} + +artifacts { + archives shadowJar +} + +// CurseForge does not allow mods that download dependencies from external sources at runtime. +// Therefore, we create this special "loader-with-deps" jar which bundles all dependencies into +// the mod jar itself. This way, no runtime downloading is necessary. + +// The dependencies list below is auto-generated via `DependencyExportTest` in the common test source set. +def deps = [ + [name: "asm-9.8.jarinjar", url: "https://repo1.maven.org/maven2/org/ow2/asm/asm/9.8/asm-9.8.jar", sha256: "h26raoPa7K1cpn65/KuwY8l7WuuM8fynqYns3hdSIFE="], + [name: "asm-commons-9.8.jarinjar", url: "https://repo1.maven.org/maven2/org/ow2/asm/asm-commons/9.8/asm-commons-9.8.jar", sha256: "MwGhwctMWfzFKSZI2sHXxa7UwPBn376IhzuM3+d0BPQ="], + [name: "jar-relocator-1.7.jarinjar", url: "https://repo1.maven.org/maven2/me/lucko/jar-relocator/1.7/jar-relocator-1.7.jar", sha256: "b30RhOF6kHiHl+O5suNLh/+eAr1iOFEFLXhwkHHDu4I="], + [name: "adventure-4.21.1.jarinjar", url: "https://repo1.maven.org/maven2/me/lucko/adventure-api/4.21.1/adventure-api-4.21.1.jar", sha256: "kQJlZ0gUxdTRRkskT43qiy2kpt9s654LvB0nqoCP6YE="], + [name: "event-3.0.0.jarinjar", url: "https://repo1.maven.org/maven2/net/kyori/event-api/3.0.0/event-api-3.0.0.jar", sha256: "yjvdTdAyktl3iFEQFLHC3qYwwt7/DbCd7Zc8Q4SlIag="], + [name: "caffeine-3.2.0.jarinjar", url: "https://repo1.maven.org/maven2/com/github/ben-manes/caffeine/caffeine/3.2.0/caffeine-3.2.0.jar", sha256: "7EEd/fDAPyUhhkjOiYYWMLcWgOWFippyeOusjlXKs9c="], + [name: "okio-1.17.6.jarinjar", url: "https://repo1.maven.org/maven2/com/squareup/okio/okio/1.17.6/okio-1.17.6.jar", sha256: "joiwVVI8yAYT37hE1Zh0DhCtpi9L2YMEzdFAxYVMw7Y="], + [name: "okhttp-3.14.9.jarinjar", url: "https://repo1.maven.org/maven2/com/squareup/okhttp3/okhttp/3.14.9/okhttp-3.14.9.jar", sha256: "JXD6tVUVy/iB16TO70n8UVSQvAJwV+Zmd2ooMkZa7KA="], + [name: "bytebuddy-1.15.11.jarinjar", url: "https://repo1.maven.org/maven2/net/bytebuddy/byte-buddy/1.15.11/byte-buddy-1.15.11.jar", sha256: "+giZiq4ee9roO94HEsUOhETXHA4MGWuyJHrejUrQ65A="], + [name: "commodore-2.2.jarinjar", url: "https://repo1.maven.org/maven2/me/lucko/commodore/2.2/commodore-2.2.jar", sha256: "hmZ3A/Sf8LvrT95buTlFNwdEBZ36X9Ks8SKOS1b7f28="], + [name: "commodore-file-1.0.jarinjar", url: "https://repo1.maven.org/maven2/me/lucko/commodore-file/1.0/commodore-file-1.0.jar", sha256: "V9++dyp9RbzD4DLO2R9upF8Z8v5SWasyX8ocqYRAMow="], + [name: "mariadb-driver-3.5.2.jarinjar", url: "https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/3.5.2/mariadb-java-client-3.5.2.jar", sha256: "8vPDwaO9rKad0dThzYrtB1JC/HKuQUY924LjZ7OI9q0="], + [name: "mysql-driver-9.3.0.jarinjar", url: "https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/9.3.0/mysql-connector-j-9.3.0.jar", sha256: "bI5mkrUhN22JvFYYwWzer4xhhUMp9PolZ37Qh3bFu3Y="], + [name: "postgresql-driver-42.7.6.jarinjar", url: "https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.6/postgresql-42.7.6.jar", sha256: "8qHMA1LdXlxvZdut/ye+4Awy5DLGrQMNB0R/ilmDxCo="], + [name: "h2-driver-legacy-1.4.199.jarinjar", url: "https://repo1.maven.org/maven2/com/h2database/h2/1.4.199/h2-1.4.199.jar", sha256: "MSWhZ0O8a0z7thq7p4MgPx+2gjCqD9yXiY95b5ml1C4="], + [name: "h2-driver-2.1.214.jarinjar", url: "https://repo1.maven.org/maven2/com/h2database/h2/2.1.214/h2-2.1.214.jar", sha256: "1iPNwPYdIYz1SajQnxw5H/kQlhFrIuJHVHX85PvnK9A="], + [name: "sqlite-driver-3.49.1.0.jarinjar", url: "https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.49.1.0/sqlite-jdbc-3.49.1.0.jar", sha256: "XIYJ0so0HeuMb3F3iXS1ukmVx9MtfHyJ2TkqPnLDkpE="], + [name: "hikari-6.3.0.jarinjar", url: "https://repo1.maven.org/maven2/com/zaxxer/HikariCP/6.3.0/HikariCP-6.3.0.jar", sha256: "B8Y0QFmvMKE1FEIJx8i9ZmuIIxJEIuyFmGTSCdSrfKE="], + [name: "slf4j-simple-1.7.36.jarinjar", url: "https://repo1.maven.org/maven2/org/slf4j/slf4j-simple/1.7.36/slf4j-simple-1.7.36.jar", sha256: "Lzm+2UPWJN+o9BAtBXEoOhCHC2qjbxl6ilBvFHAQwQ8="], + [name: "slf4j-api-1.7.36.jarinjar", url: "https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar", sha256: "0+9XXj5JeWeNwBvx3M5RAhSTtNEft/G+itmCh3wWocA="], + [name: "mongodb-driver-core-5.5.0.jarinjar", url: "https://repo1.maven.org/maven2/org/mongodb/mongodb-driver-core/5.5.0/mongodb-driver-core-5.5.0.jar", sha256: "69tQuKep52lbYvX2YM+J6GGlYkNySXkMBDuk6BqtsJE="], + [name: "mongodb-driver-legacy-5.5.0.jarinjar", url: "https://repo1.maven.org/maven2/org/mongodb/mongodb-driver-legacy/5.5.0/mongodb-driver-legacy-5.5.0.jar", sha256: "yo/0wEdLw0/Md1xqgEd/iqiKV+t0AqAcdnS1TNAaygM="], + [name: "mongodb-driver-sync-5.5.0.jarinjar", url: "https://repo1.maven.org/maven2/org/mongodb/mongodb-driver-sync/5.5.0/mongodb-driver-sync-5.5.0.jar", sha256: "nFECiREXgMc5Ikamvmnzaxumhz75NKG+ajIhAW/ioPI="], + [name: "mongodb-driver-bson-5.5.0.jarinjar", url: "https://repo1.maven.org/maven2/org/mongodb/bson/5.5.0/bson-5.5.0.jar", sha256: "hQx5w0v/DuQvASpnGXkLuWxkhXhewDTTAmrifPWbBJQ="], + [name: "jedis-5.2.0.jarinjar", url: "https://repo1.maven.org/maven2/redis/clients/jedis/5.2.0/jedis-5.2.0.jar", sha256: "3U+9osED8xmrSVrbK8GQYTmEB0bP1MZrJ3ENGvmDgtQ="], + [name: "nats-2.21.1.jarinjar", url: "https://repo1.maven.org/maven2/io/nats/jnats/2.21.1/jnats-2.21.1.jar", sha256: "QHUHUCnCCy/oRSwoqhy0245SrvD4lwCfc+ZVmemHXLg="], + [name: "rabbitmq-5.25.0.jarinjar", url: "https://repo1.maven.org/maven2/com/rabbitmq/amqp-client/5.25.0/amqp-client-5.25.0.jar", sha256: "WqlvAFCEE56xB32UtV3GQo7KfafizFPqtEp3M5H4qo8="], + [name: "commons-pool-2-2.12.1.jarinjar", url: "https://repo1.maven.org/maven2/org/apache/commons/commons-pool2/2.12.1/commons-pool2-2.12.1.jar", sha256: "UnPIvIwNyiIRF1wNJ++9cijvrplomqwAGo4e+Ohy6e8="], + [name: "configurate-core-3.7.3.jarinjar", url: "https://repo1.maven.org/maven2/org/spongepowered/configurate-core/3.7.3/configurate-core-3.7.3.jar", sha256: "06R3WDViB84WtSkHTudV8TSPxF1eQyCyfab8L7Pvo2M="], + [name: "configurate-gson-3.7.3.jarinjar", url: "https://repo1.maven.org/maven2/org/spongepowered/configurate-gson/3.7.3/configurate-gson-3.7.3.jar", sha256: "QM+bGrgrzfwT9nvIvTHtR2TUEpun+RwlXIO/a9BU0Mc="], + [name: "configurate-yaml-3.7.3.jarinjar", url: "https://repo1.maven.org/maven2/org/spongepowered/configurate-yaml/3.7.3/configurate-yaml-3.7.3.jar", sha256: "a04vRkLhigIqiG/gdVvK7c1YiBQJ7k1q/kBNsS9OVDs="], + [name: "snakeyaml-1.33.jarinjar", url: "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.33/snakeyaml-1.33.jar", sha256: "Ef9Fl4jwoteB9WpKhtfmkgLOus0Cc9UmnErp8C8/2PA="], + [name: "configurate-hocon-3.7.3.jarinjar", url: "https://repo1.maven.org/maven2/org/spongepowered/configurate-hocon/3.7.3/configurate-hocon-3.7.3.jar", sha256: "e/UDpbIrWdJNB6yMFXtrOnnNn3nptmSv/J8n46uQPNs="], + [name: "hocon-config-1.4.1.jarinjar", url: "https://repo1.maven.org/maven2/com/typesafe/config/1.4.1/config-1.4.1.jar", sha256: "TAqn4iPHXIhAxB/Bg9TNMRgUCh7lA+PgjOZu0nlMlI8="], + [name: "configurate-toml-3.7.jarinjar", url: "https://repo1.maven.org/maven2/me/lucko/configurate/configurate-toml/3.7/configurate-toml-3.7.jar", sha256: "EmyLOfsiR74QGhkktqhexMN8tC3kg1cM1UhM5MCmxuE="], + [name: "toml4j-0.7.2.jarinjar", url: "https://repo1.maven.org/maven2/com/moandjiezana/toml/toml4j/0.7.2/toml4j-0.7.2.jar", sha256: "9UdeY+fonl22IiNImux6Vr0wNUN3IHehfCy1TBnKOiA="], +] + +// Task to download the jar-in-jar dependencies and place them in the build resources directory. +tasks.register('downloadJarInJarDeps') { + def outputDir = layout.buildDirectory.dir("resources/main/luckperms/deps") + outputs.dir(outputDir) + + inputs.property( + "files", + deps.collect { "${it.name}:${it.url}:${it.sha256}" } + ) + + doLast { + def targetDir = outputDir.get().asFile + targetDir.mkdirs() + + deps.each { f -> + def target = new File(targetDir, f.name) + + boolean needsDownload = true + + if (target.exists()) { + def actual = sha256Base64(target) + if (actual == f.sha256) { + logger.lifecycle("${f.name} checksum OK") + needsDownload = false + } else { + logger.warn("${f.name} checksum mismatch, re-downloading") + } + } + + if (needsDownload) { + logger.lifecycle("Downloading ${f.name}") + f.url.toURL().withInputStream { i -> + target.withOutputStream { it << i } + } + + def actual = sha256Base64(target) + if (actual != f.sha256) { + throw new GradleException( + "Checksum verification failed for ${f.name}\n" + + "Expected: ${f.sha256}\n" + + "Actual: ${actual}" + ) + } + } + } + } +} + +import java.security.MessageDigest + +static String sha256Base64(File file) { + MessageDigest md = MessageDigest.getInstance("SHA-256") + file.withInputStream { is -> + byte[] buffer = new byte[8192] + int read + while ((read = is.read(buffer)) != -1) { + md.update(buffer, 0, read) + } + } + return Base64.encoder.encodeToString(md.digest()) +} + +tasks.named("processResources") { + dependsOn("downloadJarInJarDeps") +} diff --git a/hytale/loader-with-deps/src/main/java/me/lucko/luckperms/hytale/loader/HytaleLoaderPlugin.java b/hytale/loader-with-deps/src/main/java/me/lucko/luckperms/hytale/loader/HytaleLoaderPlugin.java new file mode 100644 index 000000000..4883579af --- /dev/null +++ b/hytale/loader-with-deps/src/main/java/me/lucko/luckperms/hytale/loader/HytaleLoaderPlugin.java @@ -0,0 +1,68 @@ +/* + * 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.hytale.loader; + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import org.checkerframework.checker.nullness.compatqual.NonNullDecl; + +import java.util.List; + +public class HytaleLoaderPlugin extends JavaPlugin { + // marker field to indicate to the plugin that dependencies should be loaded from the jar + // read using reflection in LPHytalePlugin#createDependencyManager + public static final boolean LOAD_DEPS_FROM_JAR = true; + + private static final String JAR_NAME = "luckperms-hytale.jarinjar"; + private static final String BOOTSTRAP_CLASS = "me.lucko.luckperms.hytale.LPHytaleBootstrap"; + + private final LoaderBootstrap plugin; + + public HytaleLoaderPlugin(@NonNullDecl JavaPluginInit init) { + super(init); + JarInJarClassLoader loader = new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME); + loader.setPriorityPackagePrefixes(List.of("org.slf4j")); + this.plugin = loader.instantiatePlugin(BOOTSTRAP_CLASS, JavaPlugin.class, this); + } + + @Override + protected void setup() { + this.plugin.onLoad(); + } + + @Override + protected void start() { + this.plugin.onEnable(); + } + + @Override + protected void shutdown() { + this.plugin.onDisable(); + } + +} diff --git a/hytale/loader-with-deps/src/main/resources/icon-256.png b/hytale/loader-with-deps/src/main/resources/icon-256.png new file mode 100644 index 000000000..3e1ae1210 Binary files /dev/null and b/hytale/loader-with-deps/src/main/resources/icon-256.png differ diff --git a/hytale/loader-with-deps/src/main/resources/manifest.json b/hytale/loader-with-deps/src/main/resources/manifest.json new file mode 100644 index 000000000..9bae142c4 --- /dev/null +++ b/hytale/loader-with-deps/src/main/resources/manifest.json @@ -0,0 +1,14 @@ +{ + "Group": "LuckPerms", + "Name": "LuckPerms", + "Version": "${pluginVersion}", + "Description": "A permissions plugin", + "Authors": [{"Name": "Luck"}], + "Website": "https://luckperms.net", + "ServerVersion": "${hytaleVersion}", + "Dependencies": {}, + "OptionalDependencies": {}, + "DisabledByDefault": false, + "Main": "me.lucko.luckperms.hytale.loader.HytaleLoaderPlugin", + "IncludesAssetPack": false +} \ No newline at end of file diff --git a/hytale/loader/build.gradle b/hytale/loader/build.gradle new file mode 100644 index 000000000..440d7910b --- /dev/null +++ b/hytale/loader/build.gradle @@ -0,0 +1,35 @@ +plugins { + alias(libs.plugins.shadow) +} + +dependencies { + implementation project(':api') + implementation project(':common:loader-utils') + + compileOnly "com.hypixel.hytale:Server:${hytaleVersion}" +} + +repositories { + maven { url 'https://nexus.lucko.me/repository/maven-hytale/' } +} + +processResources { + filesMatching('manifest.json') { + expand ( + 'pluginVersion': project.ext.fullVersion, + 'hytaleVersion': hytaleVersion + ) + } +} + +shadowJar { + archiveFileName = "LuckPerms-Hytale-${project.ext.fullVersion}.jar" + + from { + project(':hytale').tasks.shadowJar.archiveFile + } +} + +artifacts { + archives shadowJar +} diff --git a/hytale/loader/src/main/java/me/lucko/luckperms/hytale/loader/HytaleLoaderPlugin.java b/hytale/loader/src/main/java/me/lucko/luckperms/hytale/loader/HytaleLoaderPlugin.java new file mode 100644 index 000000000..af7d7fa99 --- /dev/null +++ b/hytale/loader/src/main/java/me/lucko/luckperms/hytale/loader/HytaleLoaderPlugin.java @@ -0,0 +1,64 @@ +/* + * 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.hytale.loader; + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import org.checkerframework.checker.nullness.compatqual.NonNullDecl; + +import java.util.List; + +public class HytaleLoaderPlugin extends JavaPlugin { + private static final String JAR_NAME = "luckperms-hytale.jarinjar"; + private static final String BOOTSTRAP_CLASS = "me.lucko.luckperms.hytale.LPHytaleBootstrap"; + + private final LoaderBootstrap plugin; + + public HytaleLoaderPlugin(@NonNullDecl JavaPluginInit init) { + super(init); + JarInJarClassLoader loader = new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME); + loader.setPriorityPackagePrefixes(List.of("org.slf4j")); + this.plugin = loader.instantiatePlugin(BOOTSTRAP_CLASS, JavaPlugin.class, this); + } + + @Override + protected void setup() { + this.plugin.onLoad(); + } + + @Override + protected void start() { + this.plugin.onEnable(); + } + + @Override + protected void shutdown() { + this.plugin.onDisable(); + } + +} diff --git a/hytale/loader/src/main/resources/icon-256.png b/hytale/loader/src/main/resources/icon-256.png new file mode 100644 index 000000000..3e1ae1210 Binary files /dev/null and b/hytale/loader/src/main/resources/icon-256.png differ diff --git a/hytale/loader/src/main/resources/manifest.json b/hytale/loader/src/main/resources/manifest.json new file mode 100644 index 000000000..9bae142c4 --- /dev/null +++ b/hytale/loader/src/main/resources/manifest.json @@ -0,0 +1,14 @@ +{ + "Group": "LuckPerms", + "Name": "LuckPerms", + "Version": "${pluginVersion}", + "Description": "A permissions plugin", + "Authors": [{"Name": "Luck"}], + "Website": "https://luckperms.net", + "ServerVersion": "${hytaleVersion}", + "Dependencies": {}, + "OptionalDependencies": {}, + "DisabledByDefault": false, + "Main": "me.lucko.luckperms.hytale.loader.HytaleLoaderPlugin", + "IncludesAssetPack": false +} \ No newline at end of file diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleCommandManager.java b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleCommandManager.java new file mode 100644 index 000000000..8559067c4 --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleCommandManager.java @@ -0,0 +1,104 @@ +/* + * 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.hytale; + +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.CommandSender; +import com.hypixel.hytale.server.core.command.system.ParseResult; +import com.hypixel.hytale.server.core.command.system.ParserContext; +import me.lucko.luckperms.common.command.CommandManager; +import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; +import me.lucko.luckperms.common.sender.Sender; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class HytaleCommandManager extends CommandManager { + private final LPHytalePlugin plugin; + + public HytaleCommandManager(LPHytalePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + public void register() { + this.plugin.getLoader().getCommandRegistry().registerCommand(new Command()); + } + + public CompletableFuture execute(CommandSender sender, String inputString) { + Sender wrapped = this.plugin.getSenderFactory().wrap(sender); + List arguments = ArgumentTokenizer.EXECUTE.tokenizeInput(inputString); + if (!arguments.isEmpty()) { + String first = arguments.get(0); + if (first.equals("luckperms") || first.equals("lp") || first.equals("/luckperms") || first.equals("/lp")) { + arguments.remove(0); + } + } + return executeCommand(wrapped, "lp", arguments); + } + + public boolean hasPermissionForAny(CommandSender sender) { + Sender wrapped = this.plugin.getSenderFactory().wrap(sender); + return hasPermissionForAny(wrapped); + } + + // public List suggest(CommandContext ctx) { + // Sender wrapped = this.plugin.getSenderFactory().wrap(ctx.sender()); + // List arguments = ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(ctx.getInputString()); + // return tabCompleteCommand(wrapped, arguments); + // } + + private class Command extends AbstractCommand { + Command() { + super("luckperms", "LuckPerms command"); + addAliases("lp"); + setAllowsExtraArguments(true); + } + + @Override + public @Nullable CompletableFuture acceptCall(@NonNull CommandSender sender, @NonNull ParserContext parserContext, @NonNull ParseResult parseResult) { + return HytaleCommandManager.this.execute(sender, parserContext.getInputString()); + } + + @Override + protected @Nullable CompletableFuture execute(@NonNull CommandContext ctx) { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean canGeneratePermission() { + return false; + } + + @Override + public boolean hasPermission(@NonNull CommandSender sender) { + return HytaleCommandManager.this.hasPermissionForAny(sender); + } + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleConfigAdapter.java b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleConfigAdapter.java new file mode 100644 index 000000000..ff175ee2e --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleConfigAdapter.java @@ -0,0 +1,46 @@ +/* + * 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.hytale; + +import me.lucko.luckperms.common.config.generic.adapter.ConfigurateConfigAdapter; +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.loader.ConfigurationLoader; +import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; + +import java.nio.file.Path; + +public class HytaleConfigAdapter extends ConfigurateConfigAdapter implements ConfigurationAdapter { + public HytaleConfigAdapter(LuckPermsPlugin plugin, Path path) { + super(plugin, path); + } + + @Override + protected ConfigurationLoader createLoader(Path path) { + return YAMLConfigurationLoader.builder().setPath(path).build(); + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleEventBus.java b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleEventBus.java new file mode 100644 index 000000000..e35f54f38 --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleEventBus.java @@ -0,0 +1,54 @@ +/* + * 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.hytale; + +import com.hypixel.hytale.server.core.plugin.PluginBase; +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.event.AbstractEventBus; + +public class HytaleEventBus extends AbstractEventBus { + public HytaleEventBus(LPHytalePlugin plugin, LuckPermsApiProvider apiProvider) { + super(plugin, apiProvider); + + // register listener + // TODO + } + + @Override + protected PluginBase checkPlugin(Object plugin) throws IllegalArgumentException { + if (plugin instanceof PluginBase) { + return (PluginBase) plugin; + } + + throw new IllegalArgumentException("Object " + plugin + " (" + plugin.getClass().getName() + ") is not a plugin."); + } + + //public void onPluginDisable(PluginDisableEvent e) { + // Plugin plugin = e.getPlugin(); + // unregisterHandlers(plugin); + //} + +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/HytalePluginLogger.java b/hytale/src/main/java/me/lucko/luckperms/hytale/HytalePluginLogger.java new file mode 100644 index 000000000..875fbad66 --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/HytalePluginLogger.java @@ -0,0 +1,62 @@ +/* + * 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.hytale; + +import com.hypixel.hytale.logger.HytaleLogger; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; + +public class HytalePluginLogger implements PluginLogger { + private final HytaleLogger logger; + + public HytalePluginLogger(HytaleLogger logger) { + this.logger = logger; + } + + @Override + public void info(String s) { + this.logger.atInfo().log(s); + } + + @Override + public void warn(String s) { + this.logger.atWarning().log(s); + } + + @Override + public void warn(String s, Throwable t) { + this.logger.atWarning().withCause(t).log(s); + } + + @Override + public void severe(String s) { + this.logger.atSevere().log(s); + } + + @Override + public void severe(String s, Throwable t) { + this.logger.atSevere().withCause(t).log(s); + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleSchedulerAdapter.java b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleSchedulerAdapter.java new file mode 100644 index 000000000..b25f89f0c --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleSchedulerAdapter.java @@ -0,0 +1,71 @@ +/* + * 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.hytale; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.server.core.command.system.CommandSender; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import me.lucko.luckperms.common.plugin.scheduler.JavaSchedulerAdapter; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.common.sender.AbstractSender; +import me.lucko.luckperms.common.sender.Sender; + +public class HytaleSchedulerAdapter extends JavaSchedulerAdapter implements SchedulerAdapter { + + public HytaleSchedulerAdapter(LPHytaleBootstrap bootstrap) { + super(bootstrap); + } + + @Override + public void executeSync(Sender ctx, Runnable task) { + executeSync(unwrapSender(ctx), task); + } + + public void executeSync(CommandSender ctx, Runnable task) { + if (ctx instanceof PlayerRef playerRef) { + Ref ref = playerRef.getReference(); + if (ref != null) { + World world = ref.getStore().getExternalData().getWorld(); + world.execute(task); + return; + } + } + + // fallback + executeAsync(task); + } + + @SuppressWarnings("unchecked") + private static CommandSender unwrapSender(Sender sender) { + if (sender instanceof AbstractSender) { + return ((AbstractSender) sender).getSender(); + } else { + throw new IllegalArgumentException("unknown sender type: " + sender.getClass()); + } + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleSenderFactory.java b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleSenderFactory.java new file mode 100644 index 000000000..0b40a0395 --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/HytaleSenderFactory.java @@ -0,0 +1,138 @@ +/* + * 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.hytale; + +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandManager; +import com.hypixel.hytale.server.core.command.system.CommandSender; +import com.hypixel.hytale.server.core.console.ConsoleSender; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.sender.SenderFactory; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.luckperms.api.util.Tristate; + +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +public class HytaleSenderFactory extends SenderFactory { + + public HytaleSenderFactory(LPHytalePlugin plugin) { + super(plugin); + } + + @Override + protected String getName(CommandSender sender) { + if (sender instanceof PlayerRef playerRef) { + return playerRef.getUsername(); + } + return Sender.CONSOLE_NAME; + } + + @Override + protected UUID getUniqueId(CommandSender sender) { + if (sender instanceof PlayerRef playerRef) { + return playerRef.getUuid(); + } + return Sender.CONSOLE_UUID; + } + + @Override + protected void sendMessage(CommandSender sender, Component message) { + Locale locale = null; + if (sender instanceof PlayerRef playerRef) { + locale = TranslationManager.parseLocale(playerRef.getLanguage()); + } + + Component rendered = TranslationManager.render(message, locale); + sender.sendMessage(toHytaleMessage(rendered)); + } + + @Override + protected Tristate getPermissionValue(CommandSender sender, String node) { + return Tristate.of(hasPermission(sender, node)); + } + + @Override + protected boolean hasPermission(CommandSender sender, String node) { + return sender.hasPermission(node); + } + + @Override + protected void performCommand(CommandSender sender, String command) { + CommandManager.get().handleCommand(sender, command).join(); + } + + @Override + protected boolean isConsole(CommandSender sender) { + return sender instanceof ConsoleSender; + } + + public static Message toHytaleMessage(Component component) { + Message message; + if (component instanceof TextComponent text) { + message = Message.raw(text.content()); + } else { + throw new UnsupportedOperationException("Unsupported component type: " + component.getClass()); + } + + TextColor color = component.color(); + if (color != null) { + message.color(color.asHexString()); + } + + TextDecoration.State bold = component.decoration(TextDecoration.BOLD); + if (bold != TextDecoration.State.NOT_SET) { + message.bold(bold == TextDecoration.State.TRUE); + } + + TextDecoration.State italic = component.decoration(TextDecoration.ITALIC); + if (italic != TextDecoration.State.NOT_SET) { + message.italic(italic == TextDecoration.State.TRUE); + } + + ClickEvent clickEvent = component.clickEvent(); + if (clickEvent != null && clickEvent.action() == ClickEvent.Action.OPEN_URL) { + message.link(clickEvent.value()); + } + + List children = component.children().stream() + .map(HytaleSenderFactory::toHytaleMessage) + .toList(); + + if (!children.isEmpty()) { + message.insertAll(children); + } + + return message; + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/LPHytaleBootstrap.java b/hytale/src/main/java/me/lucko/luckperms/hytale/LPHytaleBootstrap.java new file mode 100644 index 000000000..714c85dd9 --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/LPHytaleBootstrap.java @@ -0,0 +1,303 @@ +/* + * 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.hytale; + +import com.hypixel.hytale.common.util.java.ManifestUtil; +import com.hypixel.hytale.server.core.HytaleServer; +import com.hypixel.hytale.server.core.auth.ProfileServiceClient.PublicGameProfile; +import com.hypixel.hytale.server.core.auth.ServerAuthManager; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.PluginClassLoader; +import com.hypixel.hytale.server.core.plugin.PluginManager; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; +import net.luckperms.api.platform.Platform; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.URL; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Bootstrap plugin for LuckPerms running on Hytale. + */ +public class LPHytaleBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { + private final JavaPlugin loader; + + /** + * The plugin logger + */ + private final PluginLogger logger; + + /** + * A scheduler adapter for the platform + */ + private final HytaleSchedulerAdapter schedulerAdapter; + + /** + * The plugin class path appender + */ + private final JarInJarClassPathAppender classPathAppender; + + /** + * The plugin instance + */ + private final LPHytalePlugin plugin; + + /** + * The time when the plugin was enabled + */ + private Instant startTime; + + // load/enable latches + private final CountDownLatch loadLatch = new CountDownLatch(1); + private final CountDownLatch enableLatch = new CountDownLatch(1); + + public LPHytaleBootstrap(JavaPlugin loader) { + this.loader = loader; + + this.logger = new HytalePluginLogger(loader.getLogger()); + this.schedulerAdapter = new HytaleSchedulerAdapter(this); + this.classPathAppender = new JarInJarClassPathAppender(getClass().getClassLoader()); + this.plugin = new LPHytalePlugin(this); + } + + // provide adapters + + @Override + public JavaPlugin getLoader() { + return this.loader; + } + + @Override + public PluginLogger getPluginLogger() { + return this.logger; + } + + @Override + public HytaleSchedulerAdapter getScheduler() { + return this.schedulerAdapter; + } + + @Override + public ClassPathAppender getClassPathAppender() { + return this.classPathAppender; + } + + @Override + public InputStream getResourceStream(String path) { + // avoid picking up resources in other mods + URL url = this.classPathAppender.getClassLoader().findResource(path); + try { + return url != null ? url.openStream() : null; + } catch (IOException e) { + return null; + } + } + + // lifecycle + + @Override + public void onLoad() { + try { + this.plugin.load(); + } finally { + this.loadLatch.countDown(); + } + } + + @Override + public void onEnable() { + this.startTime = Instant.now(); + try { + this.plugin.enable(); + } finally { + this.enableLatch.countDown(); + } + } + + @Override + public void onDisable() { + // hack - allow us to load classes during disable + ReentrantReadWriteLock lock = null; + boolean didUnlock = false; + try { + PluginManager pluginManager = HytaleServer.get().getPluginManager(); + Field lockField = PluginManager.class.getDeclaredField("lock"); + lockField.setAccessible(true); + + lock = (ReentrantReadWriteLock) lockField.get(pluginManager); + + try { + lock.writeLock().unlock(); + didUnlock = true; + } catch (IllegalMonitorStateException e) { + // ignore + } + } catch (Exception e) { + // ignore + } + + try { + this.plugin.disable(); + } finally { + if (lock != null && didUnlock) { + lock.writeLock().lock(); + } + } + } + + @Override + public CountDownLatch getEnableLatch() { + return this.enableLatch; + } + + @Override + public CountDownLatch getLoadLatch() { + return this.loadLatch; + } + + // provide information about the plugin + + @Override + public String getVersion() { + return this.loader.getManifest().getVersion().toString(); + } + + @Override + public Instant getStartupTime() { + return this.startTime; + } + + // provide information about the platform + + @Override + public Platform.Type getType() { + return Platform.Type.HYTALE; + } + + @Override + public String getServerBrand() { + return "Hytale"; + } + + @Override + public String getServerVersion() { + return ManifestUtil.getImplementationVersion(); + } + + @Override + public Path getDataDirectory() { + return this.loader.getDataDirectory().toAbsolutePath(); + } + + @Override + public Optional getPlayer(UUID uniqueId) { + return Optional.ofNullable(Universe.get().getPlayer(uniqueId)); + } + + @Override + public Optional lookupUniqueId(String username) { + ServerAuthManager authManager = ServerAuthManager.getInstance(); + String sessionToken = authManager.getSessionToken(); + if (sessionToken == null) { + return Optional.empty(); + } + + PublicGameProfile profile = authManager.getProfileServiceClient().getProfileByUsername(username, sessionToken); + return Optional.ofNullable(profile).map(PublicGameProfile::getUuid); + } + + @Override + public Optional lookupUsername(UUID uniqueId) { + ServerAuthManager authManager = ServerAuthManager.getInstance(); + String sessionToken = authManager.getSessionToken(); + if (sessionToken == null) { + return Optional.empty(); + } + + PublicGameProfile profile = authManager.getProfileServiceClient().getProfileByUuid(uniqueId, sessionToken); + return Optional.ofNullable(profile).map(PublicGameProfile::getUsername); + } + + @Override + public int getPlayerCount() { + return Universe.get().getPlayerCount(); + } + + @Override + public Collection getPlayerList() { + Collection players = Universe.get().getPlayers(); + List list = new ArrayList<>(players.size()); + for (PlayerRef player : players) { + list.add(player.getUsername()); + } + return list; + } + + @Override + public Collection getOnlinePlayers() { + Collection players = Universe.get().getPlayers(); + List list = new ArrayList<>(players.size()); + for (PlayerRef player : players) { + list.add(player.getUuid()); + } + return list; + } + + @Override + public boolean isPlayerOnline(UUID uniqueId) { + return Universe.get().getPlayer(uniqueId) != null; + } + + @Override + public @Nullable String identifyClassLoader(ClassLoader classLoader) throws ReflectiveOperationException { + if (classLoader instanceof PluginClassLoader) { + Field pluginField = PluginClassLoader.class.getDeclaredField("plugin"); + pluginField.setAccessible(true); + + JavaPlugin plugin = (JavaPlugin) pluginField.get(classLoader); + return plugin.getName(); + } + return null; + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/LPHytalePlugin.java b/hytale/src/main/java/me/lucko/luckperms/hytale/LPHytalePlugin.java new file mode 100644 index 000000000..14d1c3a6d --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/LPHytalePlugin.java @@ -0,0 +1,301 @@ +/* + * 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.hytale; + +import com.hypixel.hytale.server.core.console.ConsoleSender; +import com.hypixel.hytale.server.core.permissions.PermissionsModule; +import com.hypixel.hytale.server.core.permissions.provider.HytalePermissionsProvider; +import com.hypixel.hytale.server.core.permissions.provider.PermissionProvider; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.universe.Universe; +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import me.lucko.luckperms.common.dependencies.Dependency; +import me.lucko.luckperms.common.dependencies.DependencyManager; +import me.lucko.luckperms.common.dependencies.DependencyManagerImpl; +import me.lucko.luckperms.common.dependencies.DependencyRepository; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.model.manager.track.StandardTrackManager; +import me.lucko.luckperms.common.model.manager.user.StandardUserManager; +import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; +import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.hytale.calculator.HytaleCalculatorFactory; +import me.lucko.luckperms.hytale.listeners.HytaleCommandListUpdater; +import me.lucko.luckperms.hytale.util.VirtualGroupsCache; +import me.lucko.luckperms.hytale.context.HytaleContextManager; +import me.lucko.luckperms.hytale.context.HytalePlayerCalculator; +import me.lucko.luckperms.hytale.listeners.HytaleConnectionListener; +import me.lucko.luckperms.hytale.listeners.HytalePlatformListener; +import me.lucko.luckperms.hytale.service.LuckPermsPermissionProvider; +import me.lucko.luckperms.hytale.service.PlayerVirtualGroupsMap; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.query.QueryOptions; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * LuckPerms implementation for Hytale. + */ +public class LPHytalePlugin extends AbstractLuckPermsPlugin { + private final LPHytaleBootstrap bootstrap; + + private HytaleSenderFactory senderFactory; + private HytaleConnectionListener connectionListener; + private HytaleCommandManager commandManager; + private StandardUserManager userManager; + private StandardGroupManager groupManager; + private StandardTrackManager trackManager; + private HytaleContextManager contextManager; + + private PlayerVirtualGroupsMap playerVirtualGroupsMap; + private VirtualGroupsCache virtualGroupsCache; + private LuckPermsPermissionProvider permissionProvider; + + public LPHytalePlugin(LPHytaleBootstrap bootstrap) { + this.bootstrap = bootstrap; + } + + @Override + public LPHytaleBootstrap getBootstrap() { + return this.bootstrap; + } + + public JavaPlugin getLoader() { + return this.bootstrap.getLoader(); + } + + @Override + protected DependencyManager createDependencyManager() { + boolean loadDepsFromJar = false; + + JavaPlugin loader = this.bootstrap.getLoader(); + try { + Field loadDepsFromJarField = loader.getClass().getField("LOAD_DEPS_FROM_JAR"); + loadDepsFromJar = loadDepsFromJarField.getBoolean(loader); + } catch (Exception e) { + // ignore + } + + if (loadDepsFromJar) { + getLogger().info("Will load dependencies from local jar (jar-in-jar)"); + return new DependencyManagerImpl(this, List.of(DependencyRepository.JAR_IN_JAR)); + } + + return super.createDependencyManager(); + } + + @Override + protected void setupSenderFactory() { + this.senderFactory = new HytaleSenderFactory(this); + } + + @Override + protected Set getGlobalDependencies() { + Set dependencies = super.getGlobalDependencies(); + // required for loading the LP config + dependencies.add(Dependency.CONFIGURATE_CORE); + dependencies.add(Dependency.CONFIGURATE_YAML); + dependencies.add(Dependency.SNAKEYAML); + return dependencies; + } + + @Override + protected ConfigurationAdapter provideConfigurationAdapter() { + return new HytaleConfigAdapter(this, resolveConfig("config.yml")); + } + + @Override + protected void registerPlatformListeners() { + this.connectionListener = new HytaleConnectionListener(this); + this.connectionListener.register(this.bootstrap.getLoader().getEventRegistry()); + } + + @Override + protected MessagingFactory provideMessagingFactory() { + return new MessagingFactory<>(this); + } + + @Override + protected void registerCommands() { + this.commandManager = new HytaleCommandManager(this); + this.commandManager.register(); + } + + @Override + protected void setupManagers() { + this.userManager = new StandardUserManager(this); + this.groupManager = new StandardGroupManager(this); + this.trackManager = new StandardTrackManager(this); + } + + @Override + protected CalculatorFactory provideCalculatorFactory() { + this.virtualGroupsCache = new VirtualGroupsCache(); + return new HytaleCalculatorFactory(this); + } + + @Override + protected void setupContextManager() { + this.playerVirtualGroupsMap = new PlayerVirtualGroupsMap(); + this.contextManager = new HytaleContextManager(this, this.playerVirtualGroupsMap); + + HytalePlayerCalculator playerCalculator = new HytalePlayerCalculator(this, getConfiguration().get(ConfigKeys.DISABLED_CONTEXTS)); + playerCalculator.registerEvents(this.bootstrap.getLoader().getEventRegistry()); + playerCalculator.registerSystems(this.bootstrap.getLoader().getEntityStoreRegistry()); + this.contextManager.registerCalculator(playerCalculator); + } + + @Override + protected void setupPlatformHooks() { + // permissions + PermissionsModule permissionsModule = PermissionsModule.get(); + + // find the hytale provider + HytalePermissionsProvider hytaleProvider = null; + for (PermissionProvider provider : permissionsModule.getProviders()) { + if (provider instanceof HytalePermissionsProvider hpp) { + hytaleProvider = hpp; + break; + } + } + + // register our provider + this.permissionProvider = new LuckPermsPermissionProvider(this, hytaleProvider, this.playerVirtualGroupsMap); + permissionsModule.addProvider(this.permissionProvider); + + // remove all other providers + for (PermissionProvider provider : permissionsModule.getProviders()) { + if (provider != this.permissionProvider) { + permissionsModule.removeProvider(provider); + } + } + + if (getConfiguration().get(ConfigKeys.CHAT_FORMATTER_ENABLED)) { + getLogger().warn("The built-in LuckPerms chat formatter has been removed. Please delete the 'CHAT SETTINGS' section " + + "from your LuckPerms config.yml. We recommend that users migrate to " + + "mini-chat-formatter (https://github.com/lucko/mini-chat-formatter) or a suitable alternative." + + "Please see the LuckPerms wiki for more information: https://luckperms.net/wiki/Hytale#chat-formatting"); + } + + // general + HytalePlatformListener platformListener = new HytalePlatformListener(this); + platformListener.register(this.bootstrap.getLoader().getEventRegistry()); + } + + @Override + protected void removePlatformHooks() { + PermissionsModule permissionsModule = PermissionsModule.get(); + HytalePermissionsProvider hytaleProvider = this.permissionProvider.getHytaleProvider(); + if (hytaleProvider != null) { + permissionsModule.addProvider(hytaleProvider); + } + permissionsModule.removeProvider(this.permissionProvider); + } + + @Override + protected AbstractEventBus provideEventBus(LuckPermsApiProvider apiProvider) { + return new HytaleEventBus(this, apiProvider); + } + + @Override + protected void registerApiOnPlatform(LuckPerms api) { + + } + + @Override + protected void performFinalSetup() { + if (getConfiguration().get(ConfigKeys.UPDATE_CLIENT_COMMAND_LIST)) { + getApiProvider().getEventBus().subscribe(new HytaleCommandListUpdater(this)); + } + } + + @Override + public Optional getQueryOptionsForUser(User user) { + return this.bootstrap.getPlayer(user.getUniqueId()).map(player -> this.contextManager.getQueryOptions(player)); + } + + @Override + public Stream getOnlineSenders() { + return Stream.concat( + Stream.of(getConsoleSender()), + Universe.get().getPlayers().stream().map(p -> getSenderFactory().wrap(p)) + ); + } + + @Override + public Sender getConsoleSender() { + return getSenderFactory().wrap(ConsoleSender.INSTANCE); + } + + public HytaleSenderFactory getSenderFactory() { + return this.senderFactory; + } + + public VirtualGroupsCache getVirtualGroupsCache() { + return this.virtualGroupsCache; + } + + @Override + public AbstractConnectionListener getConnectionListener() { + return this.connectionListener; + } + + @Override + public HytaleCommandManager getCommandManager() { + return this.commandManager; + } + + @Override + public StandardUserManager getUserManager() { + return this.userManager; + } + + @Override + public StandardGroupManager getGroupManager() { + return this.groupManager; + } + + @Override + public StandardTrackManager getTrackManager() { + return this.trackManager; + } + + @Override + public HytaleContextManager getContextManager() { + return this.contextManager; + } + +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/calculator/HytaleCalculatorFactory.java b/hytale/src/main/java/me/lucko/luckperms/hytale/calculator/HytaleCalculatorFactory.java new file mode 100644 index 000000000..f88fe6f7a --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/calculator/HytaleCalculatorFactory.java @@ -0,0 +1,86 @@ +/* + * 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.hytale.calculator; + +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.cacheddata.CacheMetadata; +import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorMonitored; +import me.lucko.luckperms.common.calculator.processor.DirectProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.RegexProcessor; +import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.hytale.LPHytalePlugin; +import me.lucko.luckperms.hytale.context.HytaleContextManager; +import me.lucko.luckperms.hytale.service.VirtualGroups; +import net.luckperms.api.node.Node; +import net.luckperms.api.query.QueryOptions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class HytaleCalculatorFactory implements CalculatorFactory { + private final LPHytalePlugin plugin; + + public HytaleCalculatorFactory(LPHytalePlugin plugin) { + this.plugin = plugin; + } + + @Override + public PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata) { + List processors = new ArrayList<>(6); + + processors.add(new DirectProcessor(sourceMap)); + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { + processors.add(new RegexProcessor(sourceMap)); + } + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { + processors.add(new WildcardProcessor(sourceMap)); + } + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { + processors.add(new SpongeWildcardProcessor(sourceMap)); + } + + boolean integratedOwner = queryOptions.option(HytaleContextManager.INTEGRATED_SERVER_OWNER).orElse(false); + if (integratedOwner && this.plugin.getConfiguration().get(ConfigKeys.INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS)) { + processors.add(ServerOwnerProcessor.INSTANCE); + } + + if (this.plugin.getConfiguration().get(ConfigKeys.APPLY_HYTALE_VIRTUAL_GROUPS)) { + ImmutableSet virtualGroups = queryOptions.option(VirtualGroups.KEY).orElse(VirtualGroups.EMPTY).groups(); + processors.add(new HytaleVirtualGroupProcessor(this.plugin, virtualGroups, sourceMap)); + } + + return new PermissionCalculatorMonitored(this.plugin, metadata, processors); + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/calculator/HytaleVirtualGroupProcessor.java b/hytale/src/main/java/me/lucko/luckperms/hytale/calculator/HytaleVirtualGroupProcessor.java new file mode 100644 index 000000000..15b451cff --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/calculator/HytaleVirtualGroupProcessor.java @@ -0,0 +1,77 @@ +/* + * 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.hytale.calculator; + +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.hytale.LPHytalePlugin; +import me.lucko.luckperms.hytale.util.VirtualGroupsCache; +import net.luckperms.api.node.Node; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Permission Processor for Hytale "virtual groups". + */ +public class HytaleVirtualGroupProcessor extends AbstractPermissionProcessor implements PermissionProcessor { + public static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(HytaleVirtualGroupProcessor.class); + + private final PermissionCalculator calculator; + + public HytaleVirtualGroupProcessor(LPHytalePlugin plugin, ImmutableSet virtualGroups, Map sourceMap) { + Set groups = virtualGroups; + + VirtualGroupsCache virtualGroupsCache = plugin.getVirtualGroupsCache(); + for (String group : virtualGroupsCache.getAllVirtualGroups()) { + if (groups.contains(group)) { + continue; + } + + Node node = sourceMap.get(Inheritance.key(group)); + if (node != null && node.getValue()) { + if (groups instanceof ImmutableSet) { + groups = new HashSet<>(groups); + } + groups.add(group); + } + } + + this.calculator = virtualGroupsCache.getCalculator(ImmutableSet.copyOf(groups)); + } + + @Override + public TristateResult hasPermission(String permission) { + return RESULT_FACTORY.result(this.calculator.checkPermission(permission, CheckOrigin.INTERNAL)); + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/calculator/ServerOwnerProcessor.java b/hytale/src/main/java/me/lucko/luckperms/hytale/calculator/ServerOwnerProcessor.java similarity index 77% rename from fabric/src/main/java/me/lucko/luckperms/fabric/calculator/ServerOwnerProcessor.java rename to hytale/src/main/java/me/lucko/luckperms/hytale/calculator/ServerOwnerProcessor.java index 61519020b..513026526 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/calculator/ServerOwnerProcessor.java +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/calculator/ServerOwnerProcessor.java @@ -23,20 +23,25 @@ * SOFTWARE. */ -package me.lucko.luckperms.fabric.calculator; +package me.lucko.luckperms.hytale.calculator; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; - +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import net.luckperms.api.util.Tristate; /** - * Permission processor which is added to the owner of an Integrated server to - * simply return true if no other processors match. + * Permission processor which is added to the owner of an integrated server to simply return true if no other processors match. */ -public class ServerOwnerProcessor extends AbstractPermissionProcessor { +public class ServerOwnerProcessor extends AbstractPermissionProcessor implements PermissionProcessor { private static final TristateResult TRUE_RESULT = new TristateResult.Factory(ServerOwnerProcessor.class).result(Tristate.TRUE); + public static final ServerOwnerProcessor INSTANCE = new ServerOwnerProcessor(); + + private ServerOwnerProcessor() { + + } + @Override public TristateResult hasPermission(String permission) { return TRUE_RESULT; diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/context/HytaleContextManager.java b/hytale/src/main/java/me/lucko/luckperms/hytale/context/HytaleContextManager.java new file mode 100644 index 000000000..1ce084dab --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/context/HytaleContextManager.java @@ -0,0 +1,65 @@ +/* + * 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.hytale.context; + +import com.google.common.collect.ImmutableSet; +import com.hypixel.hytale.server.core.Constants; +import com.hypixel.hytale.server.core.modules.singleplayer.SingleplayerModule; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import me.lucko.luckperms.common.context.manager.SimpleContextManager; +import me.lucko.luckperms.hytale.LPHytalePlugin; +import me.lucko.luckperms.hytale.service.PlayerVirtualGroupsMap; +import me.lucko.luckperms.hytale.service.VirtualGroups; +import net.luckperms.api.query.OptionKey; +import net.luckperms.api.query.QueryOptions; + +import java.util.UUID; + +public class HytaleContextManager extends SimpleContextManager { + public static final OptionKey INTEGRATED_SERVER_OWNER = OptionKey.of("integrated_server_owner", Boolean.class); + + private final PlayerVirtualGroupsMap playerVirtualGroupsMap; + + public HytaleContextManager(LPHytalePlugin plugin, PlayerVirtualGroupsMap playerVirtualGroupsMap) { + super(plugin, PlayerRef.class, PlayerRef.class); + this.playerVirtualGroupsMap = playerVirtualGroupsMap; + } + + @Override + public UUID getUniqueId(PlayerRef player) { + return player.getUuid(); + } + + @Override + public void customizeQueryOptions(PlayerRef subject, QueryOptions.Builder builder) { + if (Constants.SINGLEPLAYER && SingleplayerModule.isOwner(subject)) { + builder.option(INTEGRATED_SERVER_OWNER, true); + } + + ImmutableSet groups = this.playerVirtualGroupsMap.getPlayerGroups(subject.getUuid()); + builder.option(VirtualGroups.KEY, new VirtualGroups(groups)); + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/context/HytalePlayerCalculator.java b/hytale/src/main/java/me/lucko/luckperms/hytale/context/HytalePlayerCalculator.java new file mode 100644 index 000000000..f78dba899 --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/context/HytalePlayerCalculator.java @@ -0,0 +1,185 @@ +/* + * 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.hytale.context; + +import com.hypixel.hytale.component.Archetype; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.EntityEventSystem; +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.protocol.GameMode; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.event.events.ecs.ChangeGameModeEvent; +import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.util.EnumNamer; +import me.lucko.luckperms.hytale.LPHytalePlugin; +import net.luckperms.api.context.Context; +import net.luckperms.api.context.ContextCalculator; +import net.luckperms.api.context.ContextConsumer; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.DefaultContextKeys; +import net.luckperms.api.context.ImmutableContextSet; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class HytalePlayerCalculator implements ContextCalculator { + private static final EnumNamer GAMEMODE_NAMER = new EnumNamer<>( + GameMode.class, + EnumNamer.LOWER_CASE_NAME + ); + + protected final LPHytalePlugin plugin; + + protected final boolean gamemode; + protected final boolean world; + + private final Map playerGameModes = new ConcurrentHashMap<>(); + + public HytalePlayerCalculator(LPHytalePlugin plugin, Set disabled) { + this.plugin = plugin; + this.gamemode = !disabled.contains(DefaultContextKeys.GAMEMODE_KEY); + this.world = !disabled.contains(DefaultContextKeys.WORLD_KEY); + } + + public void registerEvents(EventRegistry registry) { + registry.registerGlobal(AddPlayerToWorldEvent.class, this::onAddPlayerToWorld); + } + + public void registerSystems(ComponentRegistryProxy registry) { + registry.registerSystem(new ChangeGameModeSystem()); + } + + @Override + public void calculate(@NonNull PlayerRef target, @NonNull ContextConsumer consumer) { + Ref ref = target.getReference(); + if (ref == null || !ref.isValid()) { + return; + } + + Store store = ref.getStore(); + Player player = store.isInThread() ? store.getComponent(ref, Player.getComponentType()) : null; + + if (this.gamemode) { + GameMode mode; + if (player != null) { + mode = player.getGameMode(); + } else { + mode = this.playerGameModes.get(target.getUuid()); + } + + if (mode != null) { + consumer.accept(DefaultContextKeys.GAMEMODE_KEY, GAMEMODE_NAMER.name(mode)); + } + } + + World world = store.getExternalData().getWorld(); + if (this.world) { + this.plugin.getConfiguration().get(ConfigKeys.WORLD_REWRITES).rewriteAndSubmit(world.getName(), consumer); + } + } + + @Override + public @NonNull ContextSet estimatePotentialContexts() { + ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); + + if (this.gamemode) { + for (GameMode value : GameMode.values()) { + builder.add(DefaultContextKeys.GAMEMODE_KEY, GAMEMODE_NAMER.name(value)); + } + } + + if (this.world) { + for (World world : Universe.get().getWorlds().values()) { + String name = world.getName(); + if (Context.isValidValue(name)) { + builder.add(DefaultContextKeys.WORLD_KEY, name); + } + } + } + + return builder.build(); + } + + private void onAddPlayerToWorld(AddPlayerToWorldEvent event) { + Holder holder = event.getHolder(); + + PlayerRef playerRef = holder.getComponent(PlayerRef.getComponentType()); + if (playerRef == null) { + return; + } + + this.plugin.getContextManager().signalContextUpdate(playerRef); + + Player player = holder.getComponent(Player.getComponentType()); + if (player != null) { + GameMode gameMode = player.getGameMode(); + if (gameMode != null) { + this.playerGameModes.put(playerRef.getUuid(), gameMode); + } + } + } + + private void onGameModeEvent(PlayerRef playerRef, ChangeGameModeEvent e) { + this.playerGameModes.put(playerRef.getUuid(), e.getGameMode()); + this.plugin.getContextManager().signalContextUpdate(playerRef); + } + + private final class ChangeGameModeSystem extends EntityEventSystem { + ChangeGameModeSystem() { + super(ChangeGameModeEvent.class); + } + + @Override + public void handle(int index, ArchetypeChunk archetypeChunk, Store store, CommandBuffer commandBuffer, ChangeGameModeEvent event) { + Ref entity = archetypeChunk.getReferenceTo(index); + PlayerRef playerRef = entity.getStore().getComponent(entity, PlayerRef.getComponentType()); + if (playerRef == null) { + return; + } + onGameModeEvent(playerRef, event); + } + + @Override + public Query getQuery() { + return Archetype.empty(); + } + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/listeners/HytaleCommandListUpdater.java b/hytale/src/main/java/me/lucko/luckperms/hytale/listeners/HytaleCommandListUpdater.java new file mode 100644 index 000000000..3f223eb7f --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/listeners/HytaleCommandListUpdater.java @@ -0,0 +1,63 @@ +/* + * 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.hytale.listeners; + +import com.hypixel.hytale.server.core.io.PacketHandler; +import com.hypixel.hytale.server.core.io.handlers.game.GamePacketHandler; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import me.lucko.luckperms.common.event.listeners.AbstractCommandListUpdater; +import me.lucko.luckperms.hytale.LPHytalePlugin; + +import java.util.UUID; + +public class HytaleCommandListUpdater extends AbstractCommandListUpdater { + public HytaleCommandListUpdater(LPHytalePlugin plugin) { + super(plugin, PlayerRef.class); + } + + @Override + protected boolean isServerAvailable() { + return true; + } + + @Override + protected UUID getUniqueId(PlayerRef player) { + return player.getUuid(); + } + + @Override + protected void sendCommandListUpdate(UUID uniqueId) { + PlayerRef playerRef = Universe.get().getPlayer(uniqueId); + if (playerRef != null) { + PacketHandler handler = playerRef.getPacketHandler(); + if (handler instanceof GamePacketHandler gameHandler) { + gameHandler.sendCommandTree(); + } + } + } + +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/listeners/HytaleConnectionListener.java b/hytale/src/main/java/me/lucko/luckperms/hytale/listeners/HytaleConnectionListener.java new file mode 100644 index 000000000..d1598bf67 --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/listeners/HytaleConnectionListener.java @@ -0,0 +1,157 @@ +/* + * 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.hytale.listeners; + +import com.hypixel.hytale.event.EventPriority; +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent; +import com.hypixel.hytale.server.core.event.events.player.PlayerSetupConnectEvent; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; +import me.lucko.luckperms.hytale.HytaleSenderFactory; +import me.lucko.luckperms.hytale.LPHytalePlugin; +import net.kyori.adventure.text.Component; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class HytaleConnectionListener extends AbstractConnectionListener { + private final LPHytalePlugin plugin; + + private final Set deniedAsyncLogin = Collections.synchronizedSet(new HashSet<>()); + + public HytaleConnectionListener(LPHytalePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + public void register(EventRegistry registry) { + registry.register(EventPriority.EARLY, PlayerSetupConnectEvent.class, this::onPlayerPreLogin); + registry.register(EventPriority.LAST, PlayerSetupConnectEvent.class, this::onPlayerPreLoginMonitor); + registry.register(EventPriority.NORMAL, PlayerConnectEvent.class, this::onPlayerPostLogin); + registry.register(EventPriority.LAST, PlayerDisconnectEvent.class, this::onPlayerQuit); + } + + private void onPlayerPreLogin(PlayerSetupConnectEvent e) { + /* Called when the player first attempts a connection with the server. + Listening on LOW priority to allow plugins to modify username / UUID data here. (auth plugins) + Also, give other plugins a chance to cancel the event. */ + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login for " + e.getUuid() + " - " + e.getUsername()); + } + + if (e.isCancelled()) { + // another plugin has disallowed the login. + this.plugin.getLogger().info("Another plugin has cancelled the connection for " + e.getUuid() + " - " + e.getUsername() + ". No permissions data will be loaded."); + this.deniedAsyncLogin.add(e.getUuid()); + return; + } + + /* Actually process the login for the connection. + We do this here to delay the login until the data is ready. + If the login gets cancelled later on, then this will be cleaned up. + + This includes: + - loading uuid data + - loading permissions + - creating a user instance in the UserManager for this connection. + - setting up cached data. */ + try { + User user = loadUser(e.getUuid(), e.getUsername()); + recordConnection(e.getUuid()); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(e.getUuid(), e.getUsername(), user); + } catch (Exception ex) { + this.plugin.getLogger().severe("Exception occurred whilst loading data for " + e.getUuid() + " - " + e.getUsername(), ex); + + // deny the connection + this.deniedAsyncLogin.add(e.getUuid()); + + Component reason = TranslationManager.render(Message.LOADING_DATABASE_ERROR.build()); + e.setCancelled(true); + e.setReason(HytaleSenderFactory.toHytaleMessage(reason)); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(e.getUuid(), e.getUsername(), null); + } + } + + private void onPlayerPreLoginMonitor(PlayerSetupConnectEvent e) { + /* Listen to see if the event was cancelled after we initially handled the connection + If the connection was cancelled here, we need to do something to clean up the data that was loaded. */ + + // Check to see if this connection was denied at LOW. + if (this.deniedAsyncLogin.remove(e.getUuid())) { + // their data was never loaded at LOW priority, now check to see if they have been magically allowed since then. + + // This is a problem, as they were denied at low priority, but are now being allowed. + if (!e.isCancelled()) { + this.plugin.getLogger().severe("Player connection was re-allowed for " + e.getUuid()); + e.setCancelled(true); + } + } + } + + private void onPlayerPostLogin(PlayerConnectEvent e) { + /* Called when the player starts logging into the server. + At this point, the users data should be present and loaded. */ + + PlayerRef player = e.getPlayerRef(); + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing post-login for " + player.getUuid() + " - " + player.getUsername()); + } + + final User user = this.plugin.getUserManager().getIfLoaded(player.getUuid()); + if (user != null) { + return; + } + + if (!getUniqueConnections().contains(player.getUuid())) { + this.plugin.getLogger().warn("User " + player.getUuid() + " - " + player.getUsername() + + " doesn't have data pre-loaded, they have never been processed during pre-login in this session." + + " - denying login."); + } else { + this.plugin.getLogger().warn("User " + player.getUuid() + " - " + player.getUsername() + + " doesn't currently have data pre-loaded, but they have been processed before in this session." + + " - denying login."); + } + + Component reason = TranslationManager.render(Message.LOADING_STATE_ERROR.build(), player.getLanguage()); + player.getPacketHandler().disconnect(HytaleSenderFactory.toHytaleMessage(reason)); + } + + private void onPlayerQuit(PlayerDisconnectEvent e) { + final PlayerRef player = e.getPlayerRef(); + handleDisconnect(player.getUuid()); + } + +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/listeners/HytalePlatformListener.java b/hytale/src/main/java/me/lucko/luckperms/hytale/listeners/HytalePlatformListener.java new file mode 100644 index 000000000..f4655458c --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/listeners/HytalePlatformListener.java @@ -0,0 +1,78 @@ +/* + * 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.hytale.listeners; + +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import com.hypixel.hytale.server.core.command.system.CommandManager; +import com.hypixel.hytale.server.core.event.events.BootEvent; +import com.hypixel.hytale.server.core.permissions.PermissionsModule; +import me.lucko.luckperms.hytale.LPHytalePlugin; + +import java.util.Collection; + +public class HytalePlatformListener { + private final LPHytalePlugin plugin; + + public HytalePlatformListener(LPHytalePlugin plugin) { + this.plugin = plugin; + } + + public void register(EventRegistry registry) { + registry.registerGlobal(BootEvent.class, this::onBoot); + } + + public void onBoot(BootEvent e) { + insertCommandPermissionsIntoRegistry(); + insertRegisteredPermissionsIntoRegistry(); + this.plugin.getVirtualGroupsCache().refresh(); + } + + public void insertCommandPermissionsIntoRegistry() { + insertCommandPermissionsIntoRegistry(CommandManager.get().getCommandRegistration().values()); + } + + private void insertCommandPermissionsIntoRegistry(Collection commands) { + for (AbstractCommand command : commands) { + String permission = command.getPermission(); + if (permission != null) { + this.plugin.getPermissionRegistry().insert(permission); + } + + Collection subCommands = command.getSubCommands().values(); + if (!subCommands.isEmpty()) { + insertCommandPermissionsIntoRegistry(subCommands); + } + } + } + + public void insertRegisteredPermissionsIntoRegistry() { + for (String permission : PermissionsModule.getRegisteredPermissions().keySet()) { + this.plugin.getPermissionRegistry().insert(permission); + } + } + +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/service/LuckPermsPermissionProvider.java b/hytale/src/main/java/me/lucko/luckperms/hytale/service/LuckPermsPermissionProvider.java new file mode 100644 index 000000000..f73528669 --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/service/LuckPermsPermissionProvider.java @@ -0,0 +1,247 @@ +/* + * 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.hytale.service; + +import com.hypixel.hytale.server.core.permissions.PermissionsModule; +import com.hypixel.hytale.server.core.permissions.provider.HytalePermissionsProvider; +import com.hypixel.hytale.server.core.permissions.provider.PermissionProvider; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.hytale.LPHytalePlugin; +import net.luckperms.api.node.types.InheritanceNode; +import net.luckperms.api.util.Tristate; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +import java.util.AbstractSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class LuckPermsPermissionProvider implements PermissionProvider { + private static final String PERMISSION_PROVIDER_NAME = "LuckPerms"; + + /** LuckPerms plugin */ + private final LPHytalePlugin plugin; + /** The Hytale built-in permission provider implementation */ + private final HytalePermissionsProvider hytaleProvider; + /** The map of player virtual groups */ + private final PlayerVirtualGroupsMap playerVirtualGroupsMap; + /** Whether to delegate operations on groups/users not managed by LuckPerms to the built-in Hytale provider */ + private final boolean delegateToHytaleProvider; + /** A set of UUIDs of users who have been delegated to the Hytale provider */ + private final Set delegatedUsers = ConcurrentHashMap.newKeySet(); + + public LuckPermsPermissionProvider(LPHytalePlugin plugin, HytalePermissionsProvider hytaleProvider, PlayerVirtualGroupsMap playerVirtualGroupsMap) { + this.plugin = plugin; + this.hytaleProvider = hytaleProvider; + this.playerVirtualGroupsMap = playerVirtualGroupsMap; + this.delegateToHytaleProvider = hytaleProvider != null && plugin.getConfiguration().get(ConfigKeys.DELEGATE_TO_HYTALE_PERMISSIONS_PROVIDER); + } + + public HytalePermissionsProvider getHytaleProvider() { + return this.hytaleProvider; + } + + @Override + public String getName() { + return PERMISSION_PROVIDER_NAME; + } + + @Override + public Set getUserPermissions(@NonNull UUID userUniqueId) { + User user = this.plugin.getUserManager().getIfLoaded(userUniqueId); + if (user != null) { + return new LuckPermsPermissionsSet(user); + } else if (this.delegateToHytaleProvider) { + if (this.delegatedUsers.add(userUniqueId)) { + this.plugin.getLogger().warn("LuckPerms does not have permissions data loaded for user '" + userUniqueId + "', so their checks will be delegated to the Hytale provider."); + } + return this.hytaleProvider.getUserPermissions(userUniqueId); + } else { + return Set.of(); + } + } + + @Override + public void addUserToGroup(@NonNull UUID userUniqueId, @NonNull String groupName) { + this.playerVirtualGroupsMap.addPlayerToGroup(userUniqueId, groupName); + if (this.delegateToHytaleProvider) { + this.hytaleProvider.addUserToGroup(userUniqueId, groupName); + } + } + + @Override + public void removeUserFromGroup(@NonNull UUID userUniqueId, @NonNull String groupName) { + this.playerVirtualGroupsMap.removePlayerFromGroup(userUniqueId, groupName); + if (this.delegateToHytaleProvider) { + this.hytaleProvider.removeUserFromGroup(userUniqueId, groupName); + } + } + + @Override + public void setUserGroup(@NonNull UUID userUniqueId, @NonNull String groupName) { + this.playerVirtualGroupsMap.addPlayerToGroupAndRemoveAllOthers(userUniqueId, groupName); + if (this.delegateToHytaleProvider) { + this.hytaleProvider.setUserGroup(userUniqueId, groupName); + } + } + + @Override + public Set getGroupsForUser(@NonNull UUID userUniqueId) { + Set virtualGroups = this.playerVirtualGroupsMap.getPlayerGroups(userUniqueId); + + User user = this.plugin.getUserManager().getIfLoaded(userUniqueId); + if (user != null) { + Set groups = new HashSet<>(virtualGroups); + for (InheritanceNode node : user.getOwnInheritanceNodes(user.getQueryOptions())) { + groups.add(node.getGroupName()); + } + return groups; + + } else if (this.delegateToHytaleProvider) { + if (this.delegatedUsers.add(userUniqueId)) { + this.plugin.getLogger().warn("LuckPerms does not have permissions data loaded for user '" + userUniqueId + "', so their checks will be delegated to the Hytale provider."); + } + + Set groups = new HashSet<>(virtualGroups); + groups.addAll(this.hytaleProvider.getGroupsForUser(userUniqueId)); + return groups; + + } else { + return virtualGroups; + } + } + + @Override + public void addUserPermissions(@NonNull UUID userUniqueId, @NonNull Set permissions) { + if (this.delegateToHytaleProvider) { + this.hytaleProvider.addUserPermissions(userUniqueId, permissions); + } + } + + @Override + public void removeUserPermissions(@NonNull UUID userUniqueId, @NonNull Set permissions) { + if (this.delegateToHytaleProvider) { + this.hytaleProvider.removeUserPermissions(userUniqueId, permissions); + } + } + + @Override + public void addGroupPermissions(@NonNull String groupName, @NonNull Set permissions) { + if (this.delegateToHytaleProvider) { + this.hytaleProvider.addGroupPermissions(groupName, permissions); + } + } + + @Override + public void removeGroupPermissions(@NonNull String groupName, @NonNull Set permissions) { + if (this.delegateToHytaleProvider) { + this.hytaleProvider.removeGroupPermissions(groupName, permissions); + } + } + + @Override + public Set getGroupPermissions(@NonNull String groupName) { + return this.delegateToHytaleProvider + ? this.hytaleProvider.getGroupPermissions(groupName) + : Set.of(); + } + + @Override + public @Nullable String getGroupParent(@NonNull String groupName) { + return this.delegateToHytaleProvider + ? this.hytaleProvider.getGroupParent(groupName) + : null; + } + + @Override + public @NonNull Set getAllRegisteredGroups() { + return this.delegateToHytaleProvider + ? this.hytaleProvider.getAllRegisteredGroups() + : Set.of(); + } + + @Override + public @NonNull Set getEffectiveGroupPermissions(@NonNull String groupName) { + return this.delegateToHytaleProvider + ? this.hytaleProvider.getEffectiveGroupPermissions(groupName) + : Set.of(); + } + + /** + * A permissions set that tricks {@link PermissionsModule#hasPermission(Set, String)} into always + * returning according to LuckPerms data. + */ + private static final class LuckPermsPermissionsSet extends AbstractSet { + private static final String WILDCARD_PERMISSION = "*"; + private static final String NEGATIVE_WILDCARD_PERMISSION = "-*"; + + private final User user; + + private LuckPermsPermissionsSet(User user) { + this.user = user; + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof String permission)) { + throw new IllegalArgumentException("Not a string: " + o); + } + + if (WILDCARD_PERMISSION.equals(permission) || NEGATIVE_WILDCARD_PERMISSION.equals(permission)) { + return false; // let LuckPerms handle wildcards itself + } + + boolean inverted = false; + if (!permission.isEmpty() && permission.charAt(0) == '-') { + inverted = true; + permission = permission.substring(1); + } + + Tristate result = this.user.getCachedData().getPermissionData().checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + return inverted != result.asBoolean(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/service/PlayerVirtualGroupsMap.java b/hytale/src/main/java/me/lucko/luckperms/hytale/service/PlayerVirtualGroupsMap.java new file mode 100644 index 000000000..745c13b3b --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/service/PlayerVirtualGroupsMap.java @@ -0,0 +1,96 @@ +/* + * 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.hytale.service; + +import com.google.common.collect.ImmutableSet; +import com.hypixel.hytale.server.core.permissions.provider.HytalePermissionsProvider; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A thread-safe map of player UUIDs to their virtual groups. + */ +public class PlayerVirtualGroupsMap { + + /** The default set of groups a player has, if not otherwise defined */ + private static final ImmutableSet DEFAULT_GROUPS = ImmutableSet.copyOf(HytalePermissionsProvider.DEFAULT_GROUP_LIST); + + private final Map> uuidToGroups = new ConcurrentHashMap<>(); + + public void addPlayerToGroup(UUID uuid, String group) { + this.uuidToGroups.compute(uuid, (key, existing)-> { + if (existing == null) { + return ImmutableSet.of(group); + } else { + if (existing.contains(group)) { + return existing; + } else { + ImmutableSet.Builder builder = ImmutableSet.builder(); + builder.addAll(existing); + builder.add(group); + return builder.build(); + } + } + }); + } + + public void removePlayerFromGroup(UUID uuid, String group) { + this.uuidToGroups.computeIfPresent(uuid, (key, existing) -> { + if (!existing.contains(group)) { + return existing; + } else { + if (existing.size() == 1) { + return null; + } + + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (String g : existing) { + if (!g.equals(group)) { + builder.add(g); + } + } + return builder.build(); + } + }); + } + + public void addPlayerToGroupAndRemoveAllOthers(UUID uuid, String group) { + this.uuidToGroups.compute(uuid, (key, existing)-> { + if (existing != null && existing.size() == 1 && existing.contains(group)) { + return existing; + } else { + return ImmutableSet.of(group); + } + }); + } + + public ImmutableSet getPlayerGroups(UUID uuid) { + return this.uuidToGroups.getOrDefault(uuid, DEFAULT_GROUPS); + } + +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/service/VirtualGroups.java b/hytale/src/main/java/me/lucko/luckperms/hytale/service/VirtualGroups.java new file mode 100644 index 000000000..a499a56a6 --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/service/VirtualGroups.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.hytale.service; + +import com.google.common.collect.ImmutableSet; +import net.luckperms.api.query.OptionKey; +import net.luckperms.api.query.QueryOptions; + +/** + * A {@link QueryOptions} option key for virtual groups. + * + * @param groups the groups + */ +public record VirtualGroups(ImmutableSet groups) { + + public static final VirtualGroups EMPTY = new VirtualGroups(ImmutableSet.of()); + public static final OptionKey KEY = OptionKey.of("virtual_groups", VirtualGroups.class); + +} diff --git a/hytale/src/main/java/me/lucko/luckperms/hytale/util/VirtualGroupsCache.java b/hytale/src/main/java/me/lucko/luckperms/hytale/util/VirtualGroupsCache.java new file mode 100644 index 000000000..6c3c1246b --- /dev/null +++ b/hytale/src/main/java/me/lucko/luckperms/hytale/util/VirtualGroupsCache.java @@ -0,0 +1,141 @@ +/* + * 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.hytale.util; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.hypixel.hytale.server.core.permissions.PermissionsModule; +import me.lucko.luckperms.common.cache.LoadingMap; +import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorBase; +import me.lucko.luckperms.common.calculator.processor.DirectProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; +import me.lucko.luckperms.common.model.InheritanceOrigin; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; +import me.lucko.luckperms.common.node.factory.NodeBuilders; +import me.lucko.luckperms.common.util.ImmutableCollectors; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.Node; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class VirtualGroupsCache { + + /** + * A loading map (cache) of virtual group names to a {@link PermissionCalculator} + * that can resolve the collective permissions of the groups. + */ + private final Map, PermissionCalculator> caches; + + /** + * A set of known virtual group names + */ + private ImmutableSet knownVirtualGroups; + + /** + * A lookup of virtual group name (lowercase) to the permissions it grants, as nodes. + */ + private ImmutableMap> virtualGroupToNodesLookup; + + public VirtualGroupsCache() { + this.caches = LoadingMap.of(this::buildCalculator); + refresh(); + } + + public void refresh() { + Map> virtualGroups = PermissionsModule.get().getVirtualGroups(); + this.knownVirtualGroups = ImmutableSet.copyOf(virtualGroups.keySet()); + this.virtualGroupToNodesLookup = virtualGroups.entrySet().stream().collect(ImmutableCollectors.toMap( + e -> e.getKey().toLowerCase(Locale.ROOT), + e -> hytalePermissionStringsToNodes(e.getValue(), e.getKey()) + )); + this.caches.clear(); + } + + public ImmutableSet getAllVirtualGroups() { + return this.knownVirtualGroups; + } + + public PermissionCalculator getCalculator(ImmutableSet virtualGroups) { + return this.caches.get(virtualGroups); + } + + private PermissionCalculator buildCalculator(Set virtualGroups) { + Map sourceMap = new ConcurrentHashMap<>(); + for (String virtualGroup : virtualGroups) { + sourceMap.putAll(this.virtualGroupToNodesLookup.getOrDefault(virtualGroup.toLowerCase(Locale.ROOT), ImmutableMap.of())); + } + + if (sourceMap.isEmpty()) { + return PermissionCalculator.EMPTY; + } + + List processors = new ArrayList<>(2); + processors.add(new DirectProcessor(sourceMap)); + processors.add(new WildcardProcessor(sourceMap)); + + return new PermissionCalculatorBase(processors); + } + + /** + * Transforms a set of Hytale permission strings into a map of {@link Node} ready for lookup. + * + * @param permissions the input + * @return the transformed map + */ + private static ImmutableMap hytalePermissionStringsToNodes(Set permissions, String originGroup) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + + InheritanceOrigin origin = new InheritanceOrigin( + new PermissionHolderIdentifier("virtual_group", originGroup), + DataType.TRANSIENT + ); + + for (String permission : permissions) { + boolean value = true; + if (!permission.isEmpty() && permission.charAt(0) == '-') { + value = false; + permission = permission.substring(1); + } + + Node node = NodeBuilders.determineMostApplicable(permission) + .value(value) + .withMetadata(InheritanceOrigin.KEY, origin) + .build(); + + builder.put(permission.toLowerCase(Locale.ROOT), node); + } + + return builder.build(); + } + +} diff --git a/hytale/src/main/resources/config.yml b/hytale/src/main/resources/config.yml new file mode 100644 index 000000000..8fdd78218 --- /dev/null +++ b/hytale/src/main/resources/config.yml @@ -0,0 +1,644 @@ +#################################################################################################### +# +----------------------------------------------------------------------------------------------+ # +# | __ __ ___ __ __ | # +# | | | | / ` |__/ |__) |__ |__) |\/| /__` | # +# | |___ \__/ \__, | \ | |___ | \ | | .__/ | # +# | | # +# | https://luckperms.net | # +# | | # +# | WIKI: https://luckperms.net/wiki | # +# | DISCORD: https://discord.gg/luckperms | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # +# | | # +# | Each option in this file is documented and explained here: | # +# | ==> https://luckperms.net/wiki/Configuration | # +# | | # +# | New options are not added to this file automatically. Default values are used if an | # +# | option cannot be found. The latest config versions can be obtained at the link above. | # +# +----------------------------------------------------------------------------------------------+ # +#################################################################################################### + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | ESSENTIAL SETTINGS | # +# | | # +# | Important settings that control how LuckPerms functions. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The name of the server, used for server specific permissions. +# +# - When set to "global" this setting is effectively ignored. +# - In all other cases, the value here is added to all players in a "server" context. +# - See: https://luckperms.net/wiki/Context +server: global + +# If the servers own UUID cache/lookup facility should be used when there is no record for a player +# already in LuckPerms. +# +# - When this is set to 'false', commands using a player's username will not work unless the player +# has joined since LuckPerms was first installed. +# - To get around this, you can use a player's uuid directly in the command, or enable this option. +# - When this is set to 'true', the server facility is used. This may use a number of methods, +# including checking the servers local cache, or making a request to the Mojang API. +use-server-uuid-cache: false + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | STORAGE SETTINGS | # +# | | # +# | Controls which storage method LuckPerms will use to store data. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# How the plugin should store data +# +# - The various options are explained in more detail on the wiki: +# https://luckperms.net/wiki/Storage-types +# +# - Possible options: +# +# | Remote databases - require connection information to be configured below +# |=> MySQL +# |=> MariaDB (preferred over MySQL) +# |=> PostgreSQL +# |=> MongoDB +# +# | Flatfile/local database - don't require any extra configuration +# |=> H2 (preferred over SQLite) +# |=> SQLite +# +# | Readable & editable text files - don't require any extra configuration +# |=> YAML (.yml files) +# |=> JSON (.json files) +# |=> HOCON (.conf files) +# |=> TOML (.toml files) +# | +# | By default, user, group and track data is separated into different files. Data can be combined +# | and all stored in the same file by switching to a combined storage variant. +# | Just add '-combined' to the end of the storage-method, e.g. 'yaml-combined' +# +# - A H2 database is the default option. +# - If you want to edit data manually in "traditional" storage files, we suggest using YAML. +storage-method: h2 + +# The following block defines the settings for remote database storage methods. +# +# - You don't need to touch any of the settings here if you're using a local storage method! +# - The connection detail options are shared between all remote storage types. +data: + + # Define the address and port for the database. + # - The standard DB engine port is used by default + # (MySQL: 3306, PostgreSQL: 5432, MongoDB: 27017) + # - Specify as "host:port" if differs + address: localhost + + # The name of the database to store LuckPerms data in. + # - This must be created already. Don't worry about this setting if you're using MongoDB. + database: luckperms + + # Credentials for the database. + username: root + password: '' + + # These settings apply to the MySQL connection pool. + # - The default values will be suitable for the majority of users. + # - Do not change these settings unless you know what you're doing! + pool-settings: + + # Sets the maximum size of the MySQL connection pool. + # - Basically this value will determine the maximum number of actual + # connections to the database backend. + # - More information about determining the size of connection pools can be found here: + # https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + maximum-pool-size: 10 + + # Sets the minimum number of idle connections that the pool will try to maintain. + # - For maximum performance and responsiveness to spike demands, it is recommended to not set + # this value and instead allow the pool to act as a fixed size connection pool. + # (set this value to the same as 'maximum-pool-size') + minimum-idle: 10 + + # This setting controls the maximum lifetime of a connection in the pool in milliseconds. + # - The value should be at least 30 seconds less than any database or infrastructure imposed + # connection time limit. + maximum-lifetime: 1800000 # 30 minutes + + # This setting controls how frequently the pool will 'ping' a connection in order to prevent it + # from being timed out by the database or network infrastructure, measured in milliseconds. + # - The value should be less than maximum-lifetime and greater than 30000 (30 seconds). + # - Setting the value to zero will disable the keepalive functionality. + keepalive-time: 0 + + # This setting controls the maximum number of milliseconds that the plugin will wait for a + # connection from the pool, before timing out. + connection-timeout: 5000 # 5 seconds + + # This setting allows you to define extra properties for connections. + # + # By default, the following options are set to enable utf8 encoding. (you may need to remove + # these if you are using PostgreSQL) + # useUnicode: true + # characterEncoding: utf8 + # + # You can also use this section to disable SSL connections, by uncommenting the 'useSSL' and + # 'verifyServerCertificate' options below. + properties: + useUnicode: true + characterEncoding: utf8 + #useSSL: false + #verifyServerCertificate: false + + # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). + # - Change this if you want to use different tables for different servers. + table-prefix: 'luckperms_' + + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. + mongodb-collection-prefix: '' + + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ + mongodb-connection-uri: '' + +# Define settings for a "split" storage setup. +# +# - This allows you to define a storage method for each type of data. +# - The connection options above still have to be correct for each type here. +split-storage: + # Don't touch this if you don't want to use split storage! + enabled: false + methods: + # These options don't need to be modified if split storage isn't enabled. + user: h2 + group: h2 + track: h2 + uuid: h2 + log: h2 + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | UPDATE PROPAGATION & MESSAGING SERVICE | # +# | | # +# | Controls the ways in which LuckPerms will sync data & notify other servers of changes. | # +# | These options are documented on greater detail on the wiki under "Instant Updates". | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# This option controls how frequently LuckPerms will perform a sync task. +# +# - A sync task will refresh all data from the storage, and ensure that the most up-to-date data is +# being used by the plugin. +# - This is disabled by default, as most users will not need it. However, if you're using a remote +# storage type without a messaging service setup, you may wish to set this to something like 3. +# - Set to -1 to disable the task completely. +sync-minutes: -1 + +# If the file watcher should be enabled. +# +# - When using a file-based storage type, LuckPerms can monitor the data files for changes, and +# automatically update when changes are detected. +# - If you don't want this feature to be active, set this option to false. +watch-files: true + +# Define which messaging service should be used by the plugin. +# +# - If enabled and configured, LuckPerms will use the messaging service to inform other connected +# servers of changes. +# - Use the command "/lp networksync" to manually push changes. +# - Data is NOT stored using this service. It is only used as a messaging platform. +# +# - If you decide to enable this feature, you should set "sync-minutes" to -1, as there is no need +# for LuckPerms to poll the database for changes. +# +# - Possible options: +# => sql Uses the SQL database to form a queue system for communication. Will only work when +# 'storage-method' is set to MySQL or MariaDB. This is chosen by default if the +# option is set to 'auto' and SQL storage is in use. Set to 'notsql' to disable this. +# => pluginmsg Uses the plugin messaging channels to communicate with the proxy. +# LuckPerms must be installed on your proxy & all connected servers backend servers. +# Won't work if you have more than one proxy. +# => lilypad Uses LilyPad pub-sub to push changes. You need to have the LilyPad-Connect plugin +# installed. +# => redis Uses Redis pub-sub to push changes. Your server connection info must be configured +# below. +# => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be +# configured below. +# => nats Uses Nats pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. +# => auto Attempts to automatically setup a messaging service using redis or sql. +messaging-service: auto + +# If LuckPerms should automatically push updates after a change has been made with a command. +auto-push-updates: true + +# If LuckPerms should push logging entries to connected servers via the messaging service. +push-log-entries: true + +# If LuckPerms should broadcast received logging entries to players on this platform. +# +# - If you have LuckPerms installed on your backend servers as well as a BungeeCord proxy, you +# should set this option to false on either your backends or your proxies, to avoid players being +# messaged twice about log entries. +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 + username: '' + password: '' + +# Settings for Nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats: + enabled: false + address: localhost + username: '' + password: '' + token: '' + +# Settings for RabbitMQ. +# Port 5672 is used by default; set address to "host:port" if differs +rabbitmq: + enabled: false + address: localhost + vhost: '/' + username: 'guest' + password: 'guest' + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | CUSTOMIZATION SETTINGS | # +# | | # +# | Settings that allow admins to customize the way LuckPerms operates. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls how temporary permissions/parents/meta should be accumulated. +# +# - The default behaviour is "deny". +# - This behaviour can also be specified when the command is executed. See the command usage +# documentation for more info. +# +# - Possible options: +# => accumulate durations will be added to the existing expiry time +# => replace durations will be replaced if the new duration is later than the current +# expiration +# => deny the command will just fail if you try to add another node with the same expiry +temporary-add-behaviour: deny + +# Controls how LuckPerms will determine a users "primary" group. +# +# - The meaning and influence of "primary groups" are explained in detail on the wiki. +# - The preferred approach is to let LuckPerms automatically determine a users primary group +# based on the relative weight of their parent groups. +# +# - Possible options: +# => stored use the value stored against the users record in the file/database +# => parents-by-weight just use the users most highly weighted parent +# => all-parents-by-weight same as above, but calculates based upon all parents inherited from +# both directly and indirectly +primary-group-calculation: parents-by-weight + +# If the plugin should check for "extra" permissions with users run LP commands. +# +# - These extra permissions allow finer control over what users can do with each command, and who +# they have access to edit. +# - The nature of the checks are documented on the wiki under "Argument based command permissions". +# - Argument based permissions are *not* static, unlike the 'base' permissions, and will depend upon +# the arguments given within the command. +argument-based-command-permissions: false + +# If the plugin should check whether senders are a member of a given group before they're able to +# edit the groups data or add/remove other users to/from it. +# Note: these limitations do not apply to the web editor! +require-sender-group-membership-to-modify: false + +# If the plugin should send log notifications to users whenever permissions are modified. +# +# - Notifications are only sent to those with the appropriate permission to receive them +# - They can also be temporarily enabled/disabled on a per-user basis using +# '/lp log notify ' +log-notify: true + +# Defines a list of log entries which should not be sent as notifications to users. +# +# - Each entry in the list is a RegEx expression which is matched against the log entry description. +log-notify-filtered-descriptions: +# - "parent add example" + +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations: true + +# Defines the options for prefix and suffix stacking. +# +# - The feature allows you to display multiple prefixes or suffixes alongside a players username in +# chat. +# - It is explained and documented in more detail on the wiki under "Prefix & Suffix Stacking". +# +# - The options are divided into separate sections for prefixes and suffixes. +# - The 'duplicates' setting refers to how duplicate elements are handled. Can be 'retain-all', +# 'first-only' or 'last-only'. +# - The value of 'start-spacer' is included at the start of the resultant prefix/suffix. +# - The value of 'end-spacer' is included at the end of the resultant prefix/suffix. +# - The value of 'middle-spacer' is included between each element in the resultant prefix/suffix. +# +# - Possible format options: +# => highest Selects the value with the highest weight, from all values +# held by or inherited by the player. +# +# => lowest Same as above, except takes the one with the lowest weight. +# +# => highest_own Selects the value with the highest weight, but will not +# accept any inherited values. +# +# => lowest_own Same as above, except takes the value with the lowest weight. +# +# => highest_inherited Selects the value with the highest weight, but will only +# accept inherited values. +# +# => lowest_inherited Same as above, except takes the value with the lowest weight. +# +# => highest_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group on the given track. +# +# => lowest_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group not on the given track. +# +# => lowest_not_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_from_group_ Selects the value with the highest weight, but only if the +# value was inherited from the given group. +# +# => lowest_from_group_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_from_group_ Selects the value with the highest weight, but only if the +# value was not inherited from the given group. +# +# => lowest_not_from_group_ Same as above, except takes the value with the lowest weight. +meta-formatting: + prefix: + format: + - "highest" + duplicates: first-only + start-spacer: "" + middle-spacer: " " + end-spacer: "" + suffix: + format: + - "highest" + duplicates: first-only + start-spacer: "" + middle-spacer: " " + end-spacer: "" + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | PERMISSION CALCULATION AND INHERITANCE | # +# | | # +# | Modify the way permission checks, meta lookups and inheritance resolutions are handled. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The algorithm LuckPerms should use when traversing the "inheritance tree". +# +# - Possible options: +# => breadth-first See: https://en.wikipedia.org/wiki/Breadth-first_search +# => depth-first-pre-order See: https://en.wikipedia.org/wiki/Depth-first_search +# => depth-first-post-order See: https://en.wikipedia.org/wiki/Depth-first_search +inheritance-traversal-algorithm: depth-first-pre-order + +# If a final sort according to "inheritance rules" should be performed after the traversal algorithm +# has resolved the inheritance tree. +# +# "Inheritance rules" refers to things such as group weightings, primary group status, and the +# natural contextual ordering of the group nodes. +# +# Setting this to 'true' will allow for the inheritance rules to take priority over the structure of +# the inheritance tree. +# +# Effectively when this setting is 'true': the tree is flattened, and rules applied afterwards, +# and when this setting is 'false':, the rules are just applied during each step of the traversal. +post-traversal-inheritance-sort: false + +# Defines the mode used to determine whether a set of contexts are satisfied. +# +# - Possible options: +# => at-least-one-value-per-key Set A will be satisfied by another set B, if at least one of the +# key-value entries per key in A are also in B. +# => all-values-per-key Set A will be satisfied by another set B, if all key-value +# entries in A are also in B. +context-satisfy-mode: at-least-one-value-per-key + +# LuckPerms has a number of built-in contexts. These can be disabled by adding the context key to +# the list below. +disabled-contexts: +# - "world" + +# +----------------------------------------------------------------------------------------------+ # +# | Permission resolution settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If users on this server should have their global permissions applied. +# When set to false, only server specific permissions will apply for users on this server +include-global: true + +# If users on this server should have their global world permissions applied. +# When set to false, only world specific permissions will apply for users on this server +include-global-world: true + +# If users on this server should have global (non-server specific) groups applied +apply-global-groups: true + +# If users on this server should have global (non-world specific) groups applied +apply-global-world-groups: true + +# +----------------------------------------------------------------------------------------------+ # +# | Meta lookup settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Defines how meta values should be selected. +# +# - Possible options: +# => inheritance Selects the meta value that was inherited first +# => highest-number Selects the highest numerical meta value +# => lowest-number Selects the lowest numerical meta value +meta-value-selection-default: inheritance + +# Defines how meta values should be selected per key. +meta-value-selection: +# max-homes: highest-number + +# +----------------------------------------------------------------------------------------------+ # +# | Inheritance settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If the plugin should apply wildcard permissions. +# +# - If set to true, LuckPerms will detect wildcard permissions, and resolve & apply all registered +# permissions matching the wildcard. +apply-wildcards: true + +# If LuckPerms should resolve and apply permissions according to the Sponge style implicit wildcard +# inheritance system. +# +# - That being: If a user has been granted "example", then the player should have also be +# automatically granted "example.function", "example.another", "example.deeper.nesting", +# and so on. +apply-sponge-implicit-wildcards: false + +# If the plugin should parse regex permissions. +# +# - If set to true, LuckPerms will detect regex permissions, marked with "r=" at the start of the +# node, and resolve & apply all registered permissions matching the regex. +apply-regex: true + +# If the plugin should complete and apply shorthand permissions. +# +# - If set to true, LuckPerms will detect and expand shorthand node patterns. +apply-shorthand: true + +# If the owner of an integrated server should bypass permission checks. +# +# - This setting only applies when LuckPerms is active on a single-player world. +# - The owner of an integrated server is the player whose client instance is running the server. +integrated-server-owner-bypasses-checks: true + +# If the plugin should apply Hytale "virtual groups". These act a bit like default permissions for +# built-in commands. +# +# - If set to true, LuckPerms will consider virtual groups after LuckPerms permissions have been +# checked. +apply-hytale-virtual-groups: true + +# +----------------------------------------------------------------------------------------------+ # +# | Extra settings | # +# +----------------------------------------------------------------------------------------------+ # + +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators: [] + +# Allows you to set "aliases" for the worlds sent forward for context calculation. +# +# - These aliases are provided in addition to the real world name. Applied recursively. +# - Remove the comment characters for the default aliases to apply. +world-rewrite: +# world_nether: world +# world_the_end: world + +# Define special group weights for this server. +# +# - Group weights can also be applied directly to group data, using the setweight command. +# - This section allows weights to be set on a per-server basis. +group-weight: +# admin: 10 + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | FINE TUNING OPTIONS | # +# | | # +# | A number of more niche settings for tweaking and changing behaviour. The section also | # +# | contains toggles for some more specialised features. It is only necessary to make changes to | # +# | these options if you want to fine-tune LuckPerms behaviour. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# +----------------------------------------------------------------------------------------------+ # +# | Miscellaneous (and rarely used) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If LuckPerms should produce extra logging output when it handles logins. +# +# - Useful if you're having issues with UUID forwarding or data not being loaded. +debug-logins: false + +# If LuckPerms should allow usernames with non alphanumeric characters. +# +# - Note that due to the design of the storage implementation, usernames must still be 16 characters +# or less. +allow-invalid-usernames: false + +# If LuckPerms should not require users to confirm bulkupdate operations. +# +# - When set to true, operations will be executed immediately. +# - This is not recommended, as bulkupdate has the potential to irreversibly delete large amounts of +# data, and is not designed to be executed automatically. +# - If automation is needed, users should prefer using the LuckPerms API. +skip-bulkupdate-confirmation: false + +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate: false + +# If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. +# +# - When this happens, the plugin will set their primary group back to default. +prevent-primary-group-removal: false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode: + players: false + console: false + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands: + players: false + console: false diff --git a/neoforge/build.gradle b/neoforge/build.gradle new file mode 100644 index 000000000..28fb16def --- /dev/null +++ b/neoforge/build.gradle @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.shadow) + alias(libs.plugins.moddevgradle) +} + +tasks.withType(JavaCompile).configureEach { + options.release = 25 +} + +configurations { + shade + implementation.extendsFrom shade +} + +neoForge { + version = project.neoForgeVersion + validateAccessTransformers = true +} + +dependencies { + shade project(':common') + shade project(':common:minecraft') + compileOnly project(':common:loader-utils') +} + +shadowJar { + archiveFileName = "luckperms-neoforge.jarinjar" + configurations = [project.configurations.shade] + + dependencies { + include(dependency('me.lucko.luckperms:.*')) + } + + relocate 'net.kyori.adventure', 'me.lucko.luckperms.lib.adventure' + relocate 'net.kyori.event', 'me.lucko.luckperms.lib.eventbus' + relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine' + relocate 'okio', 'me.lucko.luckperms.lib.okio' + relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3' + relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy' + 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 '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' + relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' + relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' + relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' + relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' + relocate 'org.yaml.snakeyaml', 'me.lucko.luckperms.lib.yaml' +} + +artifacts { + archives shadowJar +} diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties new file mode 100644 index 000000000..6a648ed0f --- /dev/null +++ b/neoforge/gradle.properties @@ -0,0 +1,2 @@ +minecraftVersion=26.2 +neoForgeVersion=26.2.0.1-beta \ No newline at end of file diff --git a/neoforge/loader/build.gradle b/neoforge/loader/build.gradle new file mode 100644 index 000000000..df746c0b0 --- /dev/null +++ b/neoforge/loader/build.gradle @@ -0,0 +1,75 @@ +plugins { + alias(libs.plugins.shadow) + alias(libs.plugins.moddevgradle) + id("java-library") +} + +tasks.withType(JavaCompile).configureEach { + options.release = 25 +} + +neoForge { + version = project.neoForgeVersion + validateAccessTransformers = true + + runs { + client { + client() + } + server { + server() + } + } +} + +configurations { + shade + implementation.extendsFrom shade +} + +dependencies { + shade project(':api') + shade project(':common:loader-utils') +} + +build { + dependsOn(":neoforge:build") +} + +jar { + manifest { + attributes( + 'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), + 'Implementation-Title': 'LuckPerms', + 'Implementation-Vendor': 'LuckPerms', + 'Implementation-Version': project.ext.fullVersion, + 'Specification-Title': 'luckperms', + 'Specification-Vendor': 'LuckPerms', + 'Specification-Version': '1' + ) + } +} + +processResources { + filesMatching('META-INF/neoforge.mods.toml') { + expand 'version': project.ext.fullVersion + } +} + +shadowJar { + archiveFileName = "LuckPerms-NeoForge-${project.ext.fullVersion}.jar" + configurations = [project.configurations.shade] + + from { + project(':neoforge').tasks.shadowJar.archiveFile + } + + dependencies { + include(dependency('net.luckperms:.*')) + include(dependency('me.lucko.luckperms:.*')) + } +} + +artifacts { + archives shadowJar +} diff --git a/neoforge/loader/src/main/java/me/lucko/luckperms/neoforge/loader/NeoForgeLoaderPlugin.java b/neoforge/loader/src/main/java/me/lucko/luckperms/neoforge/loader/NeoForgeLoaderPlugin.java new file mode 100644 index 000000000..5c3e5dfd1 --- /dev/null +++ b/neoforge/loader/src/main/java/me/lucko/luckperms/neoforge/loader/NeoForgeLoaderPlugin.java @@ -0,0 +1,74 @@ +/* + * 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.neoforge.loader; + +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent; +import net.neoforged.fml.loading.FMLEnvironment; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.function.Supplier; + +@Mod(value = "luckperms") +public class NeoForgeLoaderPlugin implements Supplier { + private static final Logger LOGGER = LogManager.getLogger("luckperms"); + + private static final String JAR_NAME = "luckperms-neoforge.jarinjar"; + private static final String BOOTSTRAP_CLASS = "me.lucko.luckperms.neoforge.LPNeoForgeBootstrap"; + + private final ModContainer container; + + private JarInJarClassLoader loader; + private LoaderBootstrap plugin; + + public NeoForgeLoaderPlugin(final ModContainer modContainer, final IEventBus modBus) { + this.container = modContainer; + + if (FMLEnvironment.getDist().isClient()) { + LOGGER.info("Skipping LuckPerms init (not supported on the client!)"); + return; + } + + this.loader = new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME); + modBus.addListener(this::onCommonSetup); + } + + @Override + public ModContainer get() { + return this.container; + } + + public void onCommonSetup(FMLCommonSetupEvent event) { + this.plugin = this.loader.instantiatePlugin(BOOTSTRAP_CLASS, Supplier.class, this); + this.plugin.onLoad(); + } + +} diff --git a/neoforge/loader/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/loader/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 000000000..4314d2cc7 --- /dev/null +++ b/neoforge/loader/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,16 @@ +modLoader="javafml" +loaderVersion="[1,)" +license="MIT" +issueTrackerURL="https://github.com/LuckPerms/LuckPerms/issues" +showAsResourcePack=false + +[[mods]] + modId="luckperms" + version="${version}" + displayName="LuckPerms" + displayURL="https://luckperms.net/" + logoFile="luckperms.png" + credits="Luck" + authors="Luck" + description="A permissions plugin for Minecraft servers." + displayTest="IGNORE_ALL_VERSION" \ No newline at end of file diff --git a/neoforge/loader/src/main/resources/luckperms.png b/neoforge/loader/src/main/resources/luckperms.png new file mode 100644 index 000000000..2e0ea669a Binary files /dev/null and b/neoforge/loader/src/main/resources/luckperms.png differ diff --git a/neoforge/loader/src/main/resources/pack.mcmeta b/neoforge/loader/src/main/resources/pack.mcmeta new file mode 100644 index 000000000..f6749d3ff --- /dev/null +++ b/neoforge/loader/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "LuckPerms resources", + "pack_format": 9 + } +} \ No newline at end of file diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgeBootstrap.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgeBootstrap.java new file mode 100644 index 000000000..ac9a5d2d6 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgeBootstrap.java @@ -0,0 +1,246 @@ +/* + * 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.neoforge; + +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsBootstrap; +import me.lucko.luckperms.common.minecraft.MinecraftSchedulerAdapter; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; +import me.lucko.luckperms.common.plugin.logging.Log4jPluginLogger; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.common.util.BuildInfo; +import me.lucko.luckperms.neoforge.util.NeoForgeEventBusFacade; +import net.luckperms.api.platform.Platform; +import net.minecraft.server.MinecraftServer; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.event.server.ServerAboutToStartEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import net.neoforged.neoforgespi.language.IModInfo; +import org.apache.logging.log4j.LogManager; +import org.apache.maven.artifact.versioning.ArtifactVersion; + +import java.nio.file.Path; +import java.time.Instant; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.function.Supplier; + +/** + * Bootstrap plugin for LuckPerms running on Forge. + */ +public final class LPNeoForgeBootstrap extends MinecraftLuckPermsBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { + public static final String ID = "luckperms"; + + /** + * The plugin loader + */ + private final Supplier loader; + + /** + * The plugin logger + */ + private final PluginLogger logger; + + /** + * A scheduler adapter for the platform + */ + private final MinecraftSchedulerAdapter schedulerAdapter; + + /** + * The plugin class path appender + */ + private final ClassPathAppender classPathAppender; + + /** + * A facade for the forge event bus, compatible with LP's jar-in-jar packaging + */ + private final NeoForgeEventBusFacade forgeEventBus; + + /** + * The plugin instance + */ + private final LPNeoForgePlugin plugin; + + /** + * The time when the plugin was enabled + */ + private Instant startTime; + + // load/enable latches + private final CountDownLatch loadLatch = new CountDownLatch(1); + private final CountDownLatch enableLatch = new CountDownLatch(1); + + /** + * The Minecraft server instance + */ + private MinecraftServer server; + + public LPNeoForgeBootstrap(Supplier loader) { + this.loader = loader; + this.logger = new Log4jPluginLogger(LogManager.getLogger(LPNeoForgeBootstrap.ID)); + this.schedulerAdapter = new MinecraftSchedulerAdapter(this); + this.classPathAppender = new JarInJarClassPathAppender(getClass().getClassLoader()); + this.forgeEventBus = new NeoForgeEventBusFacade(); + this.plugin = new LPNeoForgePlugin(this); + } + + // provide adapters + + @Override + public Object getLoader() { + return this.loader; + } + + @Override + public PluginLogger getPluginLogger() { + return this.logger; + } + + @Override + public MinecraftSchedulerAdapter getScheduler() { + return this.schedulerAdapter; + } + + @Override + public ClassPathAppender getClassPathAppender() { + return this.classPathAppender; + } + + public void registerListeners(Object target) { + this.forgeEventBus.register(target); + } + + // lifecycle + + @Override + public void onLoad() { // called by the loader on FMLCommonSetupEvent + this.startTime = Instant.now(); + try { + this.plugin.load(); + } finally { + this.loadLatch.countDown(); + } + + this.forgeEventBus.register(this); + this.plugin.registerEarlyListeners(); + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void onServerAboutToStart(ServerAboutToStartEvent event) { + this.server = event.getServer(); + try { + this.plugin.enable(); + } finally { + this.enableLatch.countDown(); + } + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onServerStopping(ServerStoppingEvent event) { + this.plugin.disable(); + this.forgeEventBus.unregisterAll(); + this.server = null; + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onServerStopped(ServerStoppedEvent event) { + if (this.server == null) { + return; + } + + this.plugin.disable(); + this.forgeEventBus.unregisterAll(); + this.server = null; + } + + @Override + public CountDownLatch getLoadLatch() { + return this.loadLatch; + } + + @Override + public CountDownLatch getEnableLatch() { + return this.enableLatch; + } + + @Override + public Optional getServer() { + return Optional.ofNullable(this.server); + } + + // provide information about the plugin + + @Override + public String getVersion() { + return BuildInfo.VERSION; + } + + @Override + public Instant getStartupTime() { + return this.startTime; + } + + // provide information about the platform + + @Override + public Platform.Type getType() { + return Platform.Type.NEOFORGE; + } + + @Override + public String getServerBrand() { + return ModList.get().getModContainerById("neoforge") + .map(ModContainer::getModInfo) + .map(IModInfo::getDisplayName) + .orElse("null"); + } + + @Override + public String getServerVersion() { + String forgeVersion = ModList.get().getModContainerById("neoforge") + .map(ModContainer::getModInfo) + .map(IModInfo::getVersion) + .map(ArtifactVersion::toString) + .orElse("null"); + + return getServer().map(MinecraftServer::getServerVersion).orElse("null") + "-" + forgeVersion; + } + + @Override + public Path getDataDirectory() { + return FMLPaths.CONFIGDIR.get().resolve(LPNeoForgeBootstrap.ID).toAbsolutePath(); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgePlugin.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgePlugin.java new file mode 100644 index 000000000..1a3f1a3c3 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/LPNeoForgePlugin.java @@ -0,0 +1,168 @@ +/* + * 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.neoforge; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import me.lucko.luckperms.common.dependencies.Dependency; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.common.minecraft.MinecraftLuckPermsPlugin; +import me.lucko.luckperms.common.minecraft.listeners.MinecraftAutoOpListener; +import me.lucko.luckperms.common.minecraft.listeners.MinecraftCommandListUpdater; +import me.lucko.luckperms.neoforge.context.NeoForgeContextManager; +import me.lucko.luckperms.neoforge.context.NeoForgePlayerCalculator; +import me.lucko.luckperms.neoforge.listeners.NeoForgeConnectionListener; +import me.lucko.luckperms.neoforge.listeners.NeoForgePlatformListener; +import me.lucko.luckperms.neoforge.messaging.NeoForgeMessagingFactory; +import me.lucko.luckperms.neoforge.messaging.PluginMessageMessenger; +import me.lucko.luckperms.neoforge.service.NeoForgePermissionHandlerListener; +import net.luckperms.api.LuckPerms; +import net.neoforged.fml.ModContainer; + +import java.util.Set; + +/** + * LuckPerms implementation for Forge. + */ +public class LPNeoForgePlugin extends MinecraftLuckPermsPlugin { + private NeoForgeSenderFactory senderFactory; + private NeoForgeConnectionListener connectionListener; + private NeoForgeCommandExecutor commandManager; + private NeoForgeContextManager contextManager; + + public LPNeoForgePlugin(LPNeoForgeBootstrap bootstrap) { + super(bootstrap); + } + + protected void registerEarlyListeners() { + this.connectionListener = new NeoForgeConnectionListener(this); + this.bootstrap.registerListeners(this.connectionListener); + + NeoForgePlatformListener platformListener = new NeoForgePlatformListener(this); + this.bootstrap.registerListeners(platformListener); + + NeoForgePermissionHandlerListener permissionHandlerListener = new NeoForgePermissionHandlerListener(this); + this.bootstrap.registerListeners(permissionHandlerListener); + + if (!skipCommandRegistration()) { + this.commandManager = new NeoForgeCommandExecutor(this); + this.bootstrap.registerListeners(this.commandManager); + } + + PluginMessageMessenger.registerChannel(); + } + + @Override + protected void setupSenderFactory() { + this.senderFactory = new NeoForgeSenderFactory(this); + } + + @Override + protected Set getGlobalDependencies() { + Set dependencies = super.getGlobalDependencies(); + dependencies.add(Dependency.CONFIGURATE_CORE); + dependencies.add(Dependency.CONFIGURATE_HOCON); + dependencies.add(Dependency.HOCON_CONFIG); + return dependencies; + } + + @Override + protected ConfigurationAdapter provideConfigurationAdapter() { + return new NeoForgeConfigAdapter(this, resolveConfig("luckperms.conf")); + } + + @Override + protected void registerPlatformListeners() { + // Too late for Forge, registered in #registerEarlyListeners + } + + @Override + protected MessagingFactory provideMessagingFactory() { + return new NeoForgeMessagingFactory(this); + } + + @Override + protected void registerCommands() { + // Too late for Forge, registered in #registerEarlyListeners + } + + @Override + protected void setupContextManager() { + this.contextManager = new NeoForgeContextManager(this); + + NeoForgePlayerCalculator playerCalculator = new NeoForgePlayerCalculator(this, getConfiguration().get(ConfigKeys.DISABLED_CONTEXTS)); + this.bootstrap.registerListeners(playerCalculator); + this.contextManager.registerCalculator(playerCalculator); + } + + @Override + protected void setupPlatformHooks() { + } + + @Override + protected AbstractEventBus provideEventBus(LuckPermsApiProvider provider) { + return new NeoForgeEventBus(this, provider); + } + + @Override + protected void registerApiOnPlatform(LuckPerms api) { + } + + @Override + protected void performFinalSetup() { + // register autoop listener + if (getConfiguration().get(ConfigKeys.AUTO_OP)) { + getApiProvider().getEventBus().subscribe(new MinecraftAutoOpListener(this)); + } + + // register forge command list updater + if (getConfiguration().get(ConfigKeys.UPDATE_CLIENT_COMMAND_LIST)) { + getApiProvider().getEventBus().subscribe(new MinecraftCommandListUpdater(this)); + } + } + + public NeoForgeSenderFactory getSenderFactory() { + return this.senderFactory; + } + + @Override + public NeoForgeConnectionListener getConnectionListener() { + return this.connectionListener; + } + + @Override + public NeoForgeCommandExecutor getCommandManager() { + return this.commandManager; + } + + @Override + public NeoForgeContextManager getContextManager() { + return this.contextManager; + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeCommandExecutor.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeCommandExecutor.java new file mode 100644 index 000000000..26a8812ca --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeCommandExecutor.java @@ -0,0 +1,41 @@ +/* + * 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.neoforge; + +import me.lucko.luckperms.common.minecraft.command.MinecraftCommandExecutor; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; + +public class NeoForgeCommandExecutor extends MinecraftCommandExecutor { + public NeoForgeCommandExecutor(LPNeoForgePlugin plugin) { + super(plugin); + } + + @SubscribeEvent + public void onRegisterCommands(RegisterCommandsEvent event) { + register(event.getDispatcher()); + } +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeConfigAdapter.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeConfigAdapter.java new file mode 100644 index 000000000..5cb671840 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeConfigAdapter.java @@ -0,0 +1,46 @@ +/* + * 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.neoforge; + +import me.lucko.luckperms.common.config.generic.adapter.ConfigurateConfigAdapter; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.hocon.HoconConfigurationLoader; +import ninja.leaping.configurate.loader.ConfigurationLoader; + +import java.nio.file.Path; + +public class NeoForgeConfigAdapter extends ConfigurateConfigAdapter { + public NeoForgeConfigAdapter(LuckPermsPlugin plugin, Path path) { + super(plugin, path); + } + + @Override + protected ConfigurationLoader createLoader(Path path) { + return HoconConfigurationLoader.builder().setPath(path).build(); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeEventBus.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeEventBus.java new file mode 100644 index 000000000..9936ac8fc --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeEventBus.java @@ -0,0 +1,47 @@ +/* + * 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.neoforge; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import net.neoforged.fml.ModContainer; + +public class NeoForgeEventBus extends AbstractEventBus { + public NeoForgeEventBus(LuckPermsPlugin plugin, LuckPermsApiProvider apiProvider) { + super(plugin, apiProvider); + } + + @Override + protected ModContainer checkPlugin(Object modContainer) throws IllegalArgumentException { + if (modContainer instanceof ModContainer container) { + return container; + } + + throw new IllegalArgumentException("Object " + modContainer + " (" + modContainer.getClass().getName() + ") is not a ModContainer."); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeSenderFactory.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeSenderFactory.java new file mode 100644 index 000000000..a0bf90e1b --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/NeoForgeSenderFactory.java @@ -0,0 +1,67 @@ +/* + * 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.neoforge; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.minecraft.MinecraftSenderFactory; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import me.lucko.luckperms.common.verbose.VerboseCheckTarget; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.level.ServerPlayer; + +public class NeoForgeSenderFactory extends MinecraftSenderFactory { + public NeoForgeSenderFactory(LPNeoForgePlugin plugin) { + super(plugin); + } + + @Override + protected CommandSource getSource(CommandSourceStack sender) { + return sender.source; + } + + @Override + protected Tristate getPermissionValue(CommandSourceStack commandSource, String node) { + if (commandSource.getEntity() instanceof ServerPlayer player) { + User user = getPlugin().getUserManager().getIfLoaded(player.getUUID()); + if (user == null) { + return Tristate.UNDEFINED; + } + + QueryOptions queryOptions = getPlugin().getContextManager().getQueryOptions(player); + return user.getCachedData().getPermissionData(queryOptions).checkPermission(node, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + } + + VerboseCheckTarget target = VerboseCheckTarget.internal(commandSource.getTextName()); + getPlugin().getVerboseHandler().offerPermissionCheckEvent(CheckOrigin.PLATFORM_API_HAS_PERMISSION, target, QueryOptionsImpl.DEFAULT_CONTEXTUAL, node, TristateResult.UNDEFINED); + getPlugin().getPermissionRegistry().offer(node); + return Tristate.UNDEFINED; + } +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgeContextManager.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgeContextManager.java new file mode 100644 index 000000000..3d8ddad1c --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgeContextManager.java @@ -0,0 +1,52 @@ +/* + * 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.neoforge.context; + +import me.lucko.luckperms.common.context.manager.SimpleContextManager; +import me.lucko.luckperms.common.minecraft.context.MinecraftContextManager; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.luckperms.api.query.QueryOptions; +import net.minecraft.server.level.ServerPlayer; + +import java.util.UUID; + +public class NeoForgeContextManager extends SimpleContextManager implements MinecraftContextManager { + public NeoForgeContextManager(LPNeoForgePlugin plugin) { + super(plugin, ServerPlayer.class, ServerPlayer.class); + } + + @Override + public UUID getUniqueId(ServerPlayer player) { + return player.getUUID(); + } + + @Override + public void customizeQueryOptions(ServerPlayer subject, QueryOptions.Builder builder) { + if (subject.level().getServer().isSingleplayerOwner(subject.nameAndId())) { + builder.option(INTEGRATED_SERVER_OWNER, true); + } + } +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgePlayerCalculator.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgePlayerCalculator.java new file mode 100644 index 000000000..328bb655d --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/context/NeoForgePlayerCalculator.java @@ -0,0 +1,55 @@ +/* + * 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.neoforge.context; + +import me.lucko.luckperms.common.minecraft.context.MinecraftPlayerCalculator; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; + +import java.util.Set; + +public class NeoForgePlayerCalculator extends MinecraftPlayerCalculator { + public NeoForgePlayerCalculator(LPNeoForgePlugin plugin, Set disabled) { + super(plugin, disabled); + } + + @SubscribeEvent + public void onPlayerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) { + if (this.world || this.dimensionType) { + this.plugin.getContextManager().signalContextUpdate((ServerPlayer) event.getEntity()); + } + } + + @SubscribeEvent + public void onPlayerChangeGameMode(PlayerEvent.PlayerChangeGameModeEvent event) { + if (this.gamemode) { + this.plugin.getContextManager().signalContextUpdate((ServerPlayer) event.getEntity()); + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeConnectionListener.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeConnectionListener.java new file mode 100644 index 000000000..a8291c188 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgeConnectionListener.java @@ -0,0 +1,157 @@ +/* + * 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.neoforge.listeners; + +import com.mojang.authlib.GameProfile; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import me.lucko.luckperms.neoforge.NeoForgeSenderFactory; +import me.lucko.luckperms.neoforge.util.AsyncConfigurationTask; +import net.kyori.adventure.text.Component; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketListener; +import net.minecraft.network.protocol.login.ClientboundLoginDisconnectPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ConfigurationTask; +import net.minecraft.server.network.ServerConfigurationPacketListenerImpl; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.network.event.RegisterConfigurationTasksEvent; + +import java.util.UUID; + +public class NeoForgeConnectionListener extends AbstractConnectionListener { + private static final ConfigurationTask.Type USER_LOGIN_TASK_TYPE = new ConfigurationTask.Type("luckperms:user_login"); + + private final LPNeoForgePlugin plugin; + + public NeoForgeConnectionListener(LPNeoForgePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + @SubscribeEvent + public void onGatherLoginConfigurationTasks(RegisterConfigurationTasksEvent event) { + PacketListener packetListener = event.getListener(); + if (!(packetListener instanceof ServerConfigurationPacketListenerImpl)) { + return; + } + + GameProfile gameProfile = ((ServerConfigurationPacketListenerImpl) packetListener).getOwner(); + if (gameProfile == null) { + return; + } + + String username = gameProfile.name(); + UUID uniqueId = gameProfile.id(); + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login (sync phase) for " + uniqueId + " - " + username); + } + + AsyncConfigurationTask task = new AsyncConfigurationTask( + this.plugin, + USER_LOGIN_TASK_TYPE, + () -> onPlayerNegotiationAsync(event.getListener().getConnection(), uniqueId, username), + event.getListener() + ); + event.register(task); + } + + private void onPlayerNegotiationAsync(Connection connection, UUID uniqueId, String username) { + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing pre-login (async phase) for " + uniqueId + " - " + username); + } + + /* Actually process the login for the connection. + We do this here to delay the login until the data is ready. + If the login gets cancelled later on, then this will be cleaned up. + + This includes: + - loading uuid data + - loading permissions + - creating a user instance in the UserManager for this connection. + - setting up cached data. */ + try { + User user = loadUser(uniqueId, username); + recordConnection(uniqueId); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(uniqueId, username, user); + } catch (Exception ex) { + this.plugin.getLogger().severe("Exception occurred whilst loading data for " + uniqueId + " - " + username, ex); + + if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { + Component component = TranslationManager.render(Message.LOADING_DATABASE_ERROR.build()); + connection.send(new ClientboundLoginDisconnectPacket(NeoForgeSenderFactory.toNativeText(component))); + connection.disconnect(NeoForgeSenderFactory.toNativeText(component)); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(uniqueId, username, null); + } + } + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + ServerPlayer player = (ServerPlayer) event.getEntity(); + GameProfile profile = player.getGameProfile(); + + if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { + this.plugin.getLogger().info("Processing post-login for " + profile.id() + " - " + profile.name()); + } + + User user = this.plugin.getUserManager().getIfLoaded(profile.id()); + + if (user == null) { + if (!getUniqueConnections().contains(profile.id())) { + this.plugin.getLogger().warn("User " + profile.id() + " - " + profile.name() + + " doesn't have data pre-loaded, they have never been processed during pre-login in this session."); + } else { + this.plugin.getLogger().warn("User " + profile.id() + " - " + profile.name() + + " doesn't currently have data pre-loaded, but they have been processed before in this session."); + } + + Component component = TranslationManager.render(Message.LOADING_STATE_ERROR.build(), player.getLanguage()); + if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { + player.connection.disconnect(NeoForgeSenderFactory.toNativeText(component)); + return; + } else { + player.sendSystemMessage(NeoForgeSenderFactory.toNativeText(component)); + } + } + + this.plugin.getContextManager().signalContextUpdate(player); + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) { + ServerPlayer player = (ServerPlayer) event.getEntity(); + handleDisconnect(player.getGameProfile().id()); + } + +} \ No newline at end of file diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgePlatformListener.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgePlatformListener.java new file mode 100644 index 000000000..96504bbb2 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/listeners/NeoForgePlatformListener.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.neoforge.listeners; + +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.common.minecraft.command.BrigadierInjector; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.server.players.ServerOpList; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.event.AddServerReloadListenersEvent; +import net.neoforged.neoforge.event.CommandEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; + +import java.io.IOException; +import java.util.Locale; + +public class NeoForgePlatformListener { + private final LPNeoForgePlugin plugin; + + public NeoForgePlatformListener(LPNeoForgePlugin plugin) { + this.plugin = plugin; + } + + @SubscribeEvent + public void onCommand(CommandEvent event) { + CommandContextBuilder context = event.getParseResults().getContext(); + + if (!this.plugin.getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + for (ParsedCommandNode node : context.getNodes()) { + if (!(node.getNode() instanceof LiteralCommandNode)) { + continue; + } + + String name = node.getNode().getName().toLowerCase(Locale.ROOT); + if (name.equals("op") || name.equals("deop")) { + Message.OP_DISABLED.send(this.plugin.getSenderFactory().wrap(context.getSource())); + event.setCanceled(true); + return; + } + } + } + } + + @SubscribeEvent + public void onAddReloadListener(AddServerReloadListenersEvent event) { + Commands commands = event.getServerResources().getCommands(); + BrigadierInjector.inject(this.plugin, commands.getDispatcher()); + } + + @SubscribeEvent + public void onServerStarted(ServerStartedEvent event) { + if (!this.plugin.getConfiguration().get(ConfigKeys.OPS_ENABLED)) { + ServerOpList ops = event.getServer().getPlayerList().getOps(); + ops.getEntries().clear(); + try { + ops.save(); + } catch (IOException ex) { + this.plugin.getLogger().severe("Encountered an error while saving ops", ex); + } + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/NeoForgeMessagingFactory.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/NeoForgeMessagingFactory.java new file mode 100644 index 000000000..26f35e71d --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/NeoForgeMessagingFactory.java @@ -0,0 +1,70 @@ +/* + * 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.neoforge.messaging; + +import me.lucko.luckperms.common.messaging.InternalMessagingService; +import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.luckperms.api.messenger.MessengerProvider; +import org.checkerframework.checker.nullness.qual.NonNull; + +public class NeoForgeMessagingFactory extends MessagingFactory { + public NeoForgeMessagingFactory(LPNeoForgePlugin plugin) { + super(plugin); + } + + @Override + protected InternalMessagingService getServiceFor(String messagingType) { + if (messagingType.equals("pluginmsg") || messagingType.equals("bungee") || messagingType.equals("velocity")) { + try { + return new LuckPermsMessagingService(getPlugin(), new PluginMessageMessengerProvider()); + } catch (Exception e) { + getPlugin().getLogger().severe("Exception occurred whilst enabling messaging", e); + } + } + + return super.getServiceFor(messagingType); + } + + private class PluginMessageMessengerProvider implements MessengerProvider { + + @Override + public @NonNull String getName() { + return "PluginMessage"; + } + + @Override + public @NonNull Messenger obtain(@NonNull IncomingMessageConsumer incomingMessageConsumer) { + PluginMessageMessenger messenger = new PluginMessageMessenger(getPlugin(), incomingMessageConsumer); + messenger.init(); + return messenger; + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/PluginMessageMessenger.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/PluginMessageMessenger.java new file mode 100644 index 000000000..13168b7bb --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/messaging/PluginMessageMessenger.java @@ -0,0 +1,115 @@ +/* + * 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.neoforge.messaging; + +import com.google.common.collect.Iterables; +import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.luckperms.api.messenger.IncomingMessageConsumer; +import net.luckperms.api.messenger.Messenger; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.HandlerThread; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public class PluginMessageMessenger extends AbstractPluginMessageMessenger implements Messenger { + private static final Identifier CHANNEL_ID = Identifier.parse(AbstractPluginMessageMessenger.CHANNEL); + private static final CustomPacketPayload.Type PAYLOAD_TYPE = new CustomPacketPayload.Type<>(CHANNEL_ID); + + private final LPNeoForgePlugin plugin; + + public PluginMessageMessenger(LPNeoForgePlugin plugin, IncomingMessageConsumer consumer) { + super(consumer); + this.plugin = plugin; + } + + @SubscribeEvent + private void register(final RegisterPayloadHandlersEvent event) { + event.registrar("1").executesOn(HandlerThread.NETWORK).commonBidirectional( + PAYLOAD_TYPE, + StreamCodec.of( + (bytebuf, wrapper) -> bytebuf.writeBytes(wrapper.bytes), + buf -> { + byte[] bytes = new byte[buf.readableBytes()]; + return new MessageWrapper(bytes); + } + ), + (payload, context) -> handleIncomingMessage(payload.bytes()) + ); + } + + public void init() { + this.plugin.getBootstrap().registerListeners(this); + } + + @Override + protected void sendOutgoingMessage(byte[] buf) { + AtomicReference taskRef = new AtomicReference<>(); + SchedulerTask task = this.plugin.getBootstrap().getScheduler().asyncRepeating(() -> { + ServerPlayer player = this.plugin.getBootstrap().getServer() + .map(MinecraftServer::getPlayerList) + .map(PlayerList::getPlayers) + .map(players -> Iterables.getFirst(players, null)) + .orElse(null); + + if (player == null) { + return; + } + + PacketDistributor.sendToPlayer(player, new MessageWrapper(buf)); + + SchedulerTask t = taskRef.getAndSet(null); + if (t != null) { + t.cancel(); + } + }, 10, TimeUnit.SECONDS); + taskRef.set(task); + } + + @SuppressWarnings("EmptyMethod") + public static void registerChannel() { + // do nothing - the channels are registered in the static initializer, we just + // need to make sure that is called (which it will be if this method runs) + } + + public record MessageWrapper(byte[] bytes) implements CustomPacketPayload { + @Override + public Type type() { + return PAYLOAD_TYPE; + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandler.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandler.java new file mode 100644 index 000000000..5500f01fd --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandler.java @@ -0,0 +1,162 @@ +/* + * 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.neoforge.service; + +import me.lucko.luckperms.common.cacheddata.type.MetaCache; +import me.lucko.luckperms.common.cacheddata.type.PermissionCache; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import me.lucko.luckperms.neoforge.LPNeoForgeBootstrap; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.query.QueryMode; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.server.permission.handler.IPermissionHandler; +import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContext; +import net.neoforged.neoforge.server.permission.nodes.PermissionNode; +import net.neoforged.neoforge.server.permission.nodes.PermissionType; +import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class NeoForgePermissionHandler implements IPermissionHandler { + public static final Identifier IDENTIFIER = Identifier.fromNamespaceAndPath(LPNeoForgeBootstrap.ID, "permission_handler"); + + private final LPNeoForgePlugin plugin; + private final Set> permissionNodes; + + public NeoForgePermissionHandler(LPNeoForgePlugin plugin, Collection> permissionNodes) { + this.plugin = plugin; + this.permissionNodes = Collections.unmodifiableSet(new HashSet<>(permissionNodes)); + + for (PermissionNode node : this.permissionNodes) { + this.plugin.getPermissionRegistry().insert(node.getNodeName()); + } + } + + @Override + public Identifier getIdentifier() { + return IDENTIFIER; + } + + @Override + public Set> getRegisteredNodes() { + return this.permissionNodes; + } + + @Override + public T getPermission(ServerPlayer player, PermissionNode node, PermissionDynamicContext... context) { + User user = plugin.getUserManager().getIfLoaded(player.getUUID()); + if (user != null) { + QueryOptions queryOptions = plugin.getContextManager().getQueryOptions(player); + T value = getPermissionValue(user, queryOptions, node, context); + if (value != null) { + return value; + } + } + + return node.getDefaultResolver().resolve(player, player.getUUID(), context); + } + + @Override + public T getOfflinePermission(UUID player, PermissionNode node, PermissionDynamicContext... context) { + User user = this.plugin.getUserManager().getIfLoaded(player); + + if (user != null) { + QueryOptions queryOptions = user.getQueryOptions(); + T value = getPermissionValue(user, queryOptions, node, context); + if (value != null) { + return value; + } + } + + return node.getDefaultResolver().resolve(null, player, context); + } + + @SuppressWarnings("unchecked") + private static T getPermissionValue(User user, QueryOptions queryOptions, PermissionNode node, PermissionDynamicContext... context) { + queryOptions = appendContextToQueryOptions(queryOptions, context); + String key = node.getNodeName(); + PermissionType type = node.getType(); + + // permission check + if (type == PermissionTypes.BOOLEAN) { + PermissionCache cache = user.getCachedData().getPermissionData(queryOptions); + Tristate value = cache.checkPermission(key, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result(); + if (value != Tristate.UNDEFINED) { + return (T) (Boolean) value.asBoolean(); + } + } + + // meta lookup + if (node.getType() == PermissionTypes.STRING) { + MetaCache cache = user.getCachedData().getMetaData(queryOptions); + String value = cache.getMetaOrChatMetaValue(node.getNodeName(), CheckOrigin.PLATFORM_API); + if (value != null) { + return (T) value; + } + } + + // meta lookup (integer) + if (node.getType() == PermissionTypes.INTEGER) { + MetaCache cache = user.getCachedData().getMetaData(queryOptions); + String value = cache.getMetaOrChatMetaValue(node.getNodeName(), CheckOrigin.PLATFORM_API); + if (value != null) { + try { + return (T) Integer.valueOf(Integer.parseInt(value)); + } catch (IllegalArgumentException e) { + // ignore + } + } + } + + return null; + } + + private static QueryOptions appendContextToQueryOptions(QueryOptions queryOptions, PermissionDynamicContext... context) { + if (context.length == 0 || queryOptions.mode() != QueryMode.CONTEXTUAL) { + return queryOptions; + } + + ImmutableContextSet.Builder contextBuilder = new ImmutableContextSetImpl.BuilderImpl() + .addAll(queryOptions.context()); + + for (PermissionDynamicContext dynamicContext : context) { + contextBuilder.add(dynamicContext.getDynamic().name(), dynamicContext.getSerializedValue()); + } + + return queryOptions.toBuilder().context(contextBuilder.build()).build(); + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandlerListener.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandlerListener.java new file mode 100644 index 000000000..2356c14f6 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/service/NeoForgePermissionHandlerListener.java @@ -0,0 +1,64 @@ +/* + * 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.neoforge.service; + +import me.lucko.luckperms.common.command.access.CommandPermission; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.common.ModConfigSpec; +import net.neoforged.neoforge.common.config.NeoForgeServerConfig; +import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; +import net.neoforged.neoforge.server.permission.handler.DefaultPermissionHandler; +import net.neoforged.neoforge.server.permission.nodes.PermissionNode; +import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; + +public class NeoForgePermissionHandlerListener { + private final LPNeoForgePlugin plugin; + + public NeoForgePermissionHandlerListener(LPNeoForgePlugin plugin) { + this.plugin = plugin; + } + + @SubscribeEvent + public void onPermissionGatherHandler(PermissionGatherEvent.Handler event) { + // Override the default permission handler with LuckPerms + ModConfigSpec.ConfigValue permissionHandler = NeoForgeServerConfig.INSTANCE.permissionHandler; + if (permissionHandler.get().equals(DefaultPermissionHandler.IDENTIFIER.toString())) { + permissionHandler.set(NeoForgePermissionHandler.IDENTIFIER.toString()); + } + + event.addPermissionHandler(NeoForgePermissionHandler.IDENTIFIER, permissions -> new NeoForgePermissionHandler(this.plugin, permissions)); + } + + @SubscribeEvent + public void onPermissionGatherNodes(PermissionGatherEvent.Nodes event) { + // register luckperms nodes + for (CommandPermission permission : CommandPermission.values()) { + event.addNodes(new PermissionNode<>("luckperms", permission.getNode(), PermissionTypes.BOOLEAN, (player, uuid, context) -> false)); + } + } + +} diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/AsyncConfigurationTask.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/AsyncConfigurationTask.java new file mode 100644 index 000000000..4c1c63f97 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/AsyncConfigurationTask.java @@ -0,0 +1,47 @@ +/* + * 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.neoforge.util; + +import me.lucko.luckperms.common.minecraft.util.AbstractAsyncConfigurationTask; +import me.lucko.luckperms.neoforge.LPNeoForgePlugin; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.configuration.ServerConfigurationPacketListener; + +import java.util.function.Consumer; + +public class AsyncConfigurationTask extends AbstractAsyncConfigurationTask { + private final ServerConfigurationPacketListener listener; + + public AsyncConfigurationTask(LPNeoForgePlugin plugin, Type type, Runnable task, ServerConfigurationPacketListener listener) { + super(plugin, type, task); + this.listener = listener; + } + + @Override + public void start(Consumer> send) { + start(() -> this.listener.finishCurrentTask(type())); + } +} \ No newline at end of file diff --git a/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/NeoForgeEventBusFacade.java b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/NeoForgeEventBusFacade.java new file mode 100644 index 000000000..806701557 --- /dev/null +++ b/neoforge/src/main/java/me/lucko/luckperms/neoforge/util/NeoForgeEventBusFacade.java @@ -0,0 +1,241 @@ +/* + * 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.neoforge.util; + +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import net.neoforged.bus.api.Event; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModLoadingContext; +import net.neoforged.fml.event.IModBusEvent; +import net.neoforged.neoforge.common.NeoForge; + +import java.lang.invoke.CallSite; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * A utility for registering Forge listeners for methods in a jar-in-jar. + * + *

    This differs from {@link IEventBus#register} as reflection is used for invoking the registered listeners + * instead of ASM, which is incompatible with {@link JarInJarClassLoader}

    + */ +public class NeoForgeEventBusFacade { + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + private final List listeners = new ArrayList<>(); + + /** + * Register listeners for all methods annotated with {@link SubscribeEvent} on the target object. + */ + public void register(Object target) { + for (Method method : target.getClass().getMethods()) { + // Ignore static methods, Support for these could be added, but they are not used in LuckPerms + if (Modifier.isStatic(method.getModifiers())) { + continue; + } + + // Methods require a SubscribeEvent annotation in order to be registered + SubscribeEvent subscribeEvent = method.getAnnotation(SubscribeEvent.class); + if (subscribeEvent == null) { + continue; + } + + EventType type = determineListenerType(method); + Consumer invoker = createInvokerFunction(method, target, type); + + // Determine the 'IEventBus' that this eventType should be registered to. + IEventBus eventBus; + if (IModBusEvent.class.isAssignableFrom(type.eventType)) { + eventBus = ModLoadingContext.get().getActiveContainer().getEventBus(); + } else { + eventBus = NeoForge.EVENT_BUS; + } + + addListener(eventBus, subscribeEvent, type.eventType, invoker); + + this.listeners.add(new ListenerRegistration(invoker, eventBus, target)); + } + } + + /** + * Unregister previously registered listeners on the target object. + * + * @param target the target listener + */ + public void unregister(Object target) { + this.listeners.removeIf(listener -> { + if (listener.target == target) { + listener.close(); + return true; + } else { + return false; + } + }); + } + + /** + * Unregister all listeners created through this interface. + */ + public void unregisterAll() { + for (ListenerRegistration listener : this.listeners) { + listener.close(); + } + this.listeners.clear(); + } + + /** + * A listener registration. + */ + private static final class ListenerRegistration implements AutoCloseable { + /** The lambda invoker function */ + private final Consumer invoker; + /** The event bus that the invoker was registered to */ + private final IEventBus eventBus; + /** The target listener class */ + private final Object target; + + private ListenerRegistration(Consumer invoker, IEventBus eventBus, Object target) { + this.invoker = invoker; + this.eventBus = eventBus; + this.target = target; + } + + @Override + public void close() { + this.eventBus.unregister(this.invoker); + } + } + + private static Consumer createInvokerFunction(Method method, Object target, EventType type) { + // Use the 'LambdaMetafactory' to generate a consumer which can be passed directly to an 'IEventBus' + // when registering a listener, this reduces the overhead involved when reflectively invoking methods. + try { + MethodHandle methodHandle = LOOKUP.unreflect(method); + CallSite callSite = LambdaMetafactory.metafactory( + LOOKUP, + "accept", + MethodType.methodType(Consumer.class, target.getClass()), + MethodType.methodType(void.class, Object.class), + methodHandle, + MethodType.methodType(void.class, type.eventType) + ); + + return (Consumer) callSite.getTarget().bindTo(target).invokeExact(); + } catch (Throwable t) { + throw new RuntimeException("Error whilst registering " + method, t); + } + } + + public static EventType determineListenerType(Method method) { + // Get the parameter types, this includes generic information which is required for GenericEvent + Type[] parameterTypes = method.getGenericParameterTypes(); + if (parameterTypes.length != 1) { + throw new IllegalArgumentException("" + + "Method " + method + " has @SubscribeEvent annotation. " + + "It has " + parameterTypes.length + " arguments, " + + "but event handler methods require a single argument only." + ); + } + + Type parameterType = parameterTypes[0]; + Class eventType; + Class genericType; + + if (parameterType instanceof Class) { // Non-generic event + eventType = (Class) parameterType; + genericType = null; + } else if (parameterType instanceof ParameterizedType) { // Generic event + ParameterizedType parameterizedType = (ParameterizedType) parameterType; + + // Get the event class + Type rawType = parameterizedType.getRawType(); + if (rawType instanceof Class) { + eventType = (Class) rawType; + } else { + throw new UnsupportedOperationException("Raw Type " + rawType.getClass() + " is not supported"); + } + + // Find the type of 'T' in 'GenericEvent' + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + if (typeArguments.length != 1) { + throw new IllegalArgumentException("" + + "Method " + method + " has @SubscribeEvent annotation. " + + "It has a " + eventType + " argument, " + + "but generic events require a single type argument only." + ); + } + + // Get the generic class + Type typeArgument = typeArguments[0]; + if (typeArgument instanceof Class) { + genericType = (Class) typeArgument; + } else { + throw new UnsupportedOperationException("Type Argument " + typeArgument.getClass() + " is not supported"); + } + } else { + throw new UnsupportedOperationException("Parameter Type " + parameterType.getClass() + " is not supported"); + } + + // Ensure 'eventType' is a subclass of event + if (!Event.class.isAssignableFrom(eventType)) { + throw new IllegalArgumentException("" + + "Method " + method + " has @SubscribeEvent annotation, " + + "but takes an argument that is not an Event subtype: " + eventType + ); + } + + return new EventType(eventType, genericType); + } + + private static final class EventType { + private final Class eventType; + private final Class genericType; + + private EventType(Class eventType, Class genericType) { + this.eventType = eventType; + this.genericType = genericType; + } + } + + /** + * Handles casting generics for {@link IEventBus#addListener}. + */ + @SuppressWarnings("unchecked") + private static void addListener(IEventBus eventBus, SubscribeEvent annotation, Class eventType, Consumer consumer) { + eventBus.addListener(annotation.priority(), annotation.receiveCanceled(), (Class) eventType, (Consumer) consumer); + } + +} diff --git a/neoforge/src/main/resources/luckperms.conf b/neoforge/src/main/resources/luckperms.conf new file mode 100644 index 000000000..9a5550225 --- /dev/null +++ b/neoforge/src/main/resources/luckperms.conf @@ -0,0 +1,693 @@ +#################################################################################################### +# +----------------------------------------------------------------------------------------------+ # +# | __ __ ___ __ __ | # +# | | | | / ` |__/ |__) |__ |__) |\/| /__` | # +# | |___ \__/ \__, | \ | |___ | \ | | .__/ | # +# | | # +# | https://luckperms.net | # +# | | # +# | WIKI: https://luckperms.net/wiki | # +# | DISCORD: https://discord.gg/luckperms | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # +# | | # +# | Each option in this file is documented and explained here: | # +# | ==> https://luckperms.net/wiki/Configuration | # +# | | # +# | New options are not added to this file automatically. Default values are used if an | # +# | option cannot be found. The latest config versions can be obtained at the link above. | # +# +----------------------------------------------------------------------------------------------+ # +#################################################################################################### + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | ESSENTIAL SETTINGS | # +# | | # +# | Important settings that control how LuckPerms functions. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The name of the server, used for server specific permissions. +# +# - When set to "global" this setting is effectively ignored. +# - In all other cases, the value here is added to all players in a "server" context. +# - See: https://luckperms.net/wiki/Context +server = "global" + +# If the servers own UUID cache/lookup facility should be used when there is no record for a player +# already in LuckPerms. +# +# - When this is set to 'false', commands using a player's username will not work unless the player +# has joined since LuckPerms was first installed. +# - To get around this, you can use a player's uuid directly in the command, or enable this option. +# - When this is set to 'true', the server facility is used. This may use a number of methods, +# including checking the servers local cache, or making a request to the Mojang API. +use-server-uuid-cache = false + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | STORAGE SETTINGS | # +# | | # +# | Controls which storage method LuckPerms will use to store data. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# How the plugin should store data +# +# - The various options are explained in more detail on the wiki: +# https://luckperms.net/wiki/Storage-types +# +# - Possible options: +# +# | Remote databases - require connection information to be configured below +# |=> MySQL +# |=> MariaDB (preferred over MySQL) +# |=> PostgreSQL +# |=> MongoDB +# +# | Flatfile/local database - don't require any extra configuration +# |=> H2 (preferred over SQLite) +# |=> SQLite +# +# | Readable & editable text files - don't require any extra configuration +# |=> YAML (.yml files) +# |=> JSON (.json files) +# |=> HOCON (.conf files) +# |=> TOML (.toml files) +# | +# | By default, user, group and track data is separated into different files. Data can be combined +# | and all stored in the same file by switching to a combined storage variant. +# | Just add '-combined' to the end of the storage-method, e.g. 'yaml-combined' +# +# - A H2 database is the default option. +# - If you want to edit data manually in "traditional" storage files, we suggest using YAML. +storage-method = "h2" + +# The following block defines the settings for remote database storage methods. +# +# - You don't need to touch any of the settings here if you're using a local storage method! +# - The connection detail options are shared between all remote storage types. +data { + + # Define the address and port for the database. + # - The standard DB engine port is used by default + # (MySQL = 3306, PostgreSQL = 5432, MongoDB = 27017) + # - Specify as "host:port" if differs + address = "localhost" + + # The name of the database to store LuckPerms data in. + # - This must be created already. Don't worry about this setting if you're using MongoDB. + database = "minecraft" + + # Credentials for the database. + username = "root" + password = "" + + # These settings apply to the MySQL connection pool. + # - The default values will be suitable for the majority of users. + # - Do not change these settings unless you know what you're doing! + pool-settings { + + # Sets the maximum size of the MySQL connection pool. + # - Basically this value will determine the maximum number of actual + # connections to the database backend. + # - More information about determining the size of connection pools can be found here: + # https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + maximum-pool-size = 10 + + # Sets the minimum number of idle connections that the pool will try to maintain. + # - For maximum performance and responsiveness to spike demands, it is recommended to not set + # this value and instead allow the pool to act as a fixed size connection pool. + # (set this value to the same as 'maximum-pool-size') + minimum-idle = 10 + + # This setting controls the maximum lifetime of a connection in the pool in milliseconds. + # - The value should be at least 30 seconds less than any database or infrastructure imposed + # connection time limit. + maximum-lifetime = 1800000 # 30 minutes + + # This setting controls how frequently the pool will 'ping' a connection in order to prevent it + # from being timed out by the database or network infrastructure, measured in milliseconds. + # - The value should be less than maximum-lifetime and greater than 30000 (30 seconds). + # - Setting the value to zero will disable the keepalive functionality. + keepalive-time = 0 + + # This setting controls the maximum number of milliseconds that the plugin will wait for a + # connection from the pool, before timing out. + connection-timeout = 5000 # 5 seconds + + # This setting allows you to define extra properties for connections. + # + # By default, the following options are set to enable utf8 encoding. (you may need to remove + # these if you are using PostgreSQL) + # useUnicode = true + # characterEncoding = "utf8" + # + # You can also use this section to disable SSL connections, by uncommenting the 'useSSL' and + # 'verifyServerCertificate' options below. + properties { + useUnicode = true + characterEncoding = "utf8" + #useSSL: false + #verifyServerCertificate: false + } + } + + # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). + # - Change this if you want to use different tables for different servers. + table-prefix = "luckperms_" + + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. + mongodb-collection-prefix = "" + + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ + mongodb-connection-uri = "" +} + +# Define settings for a "split" storage setup. +# +# - This allows you to define a storage method for each type of data. +# - The connection options above still have to be correct for each type here. +split-storage { + # Don't touch this if you don't want to use split storage! + enabled = false + methods { + # These options don't need to be modified if split storage isn't enabled. + user = "h2" + group = "h2" + track = "h2" + uuid = "h2" + log = "h2" + } +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | UPDATE PROPAGATION & MESSAGING SERVICE | # +# | | # +# | Controls the ways in which LuckPerms will sync data & notify other servers of changes. | # +# | These options are documented on greater detail on the wiki under "Instant Updates". | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# This option controls how frequently LuckPerms will perform a sync task. +# +# - A sync task will refresh all data from the storage, and ensure that the most up-to-date data is +# being used by the plugin. +# - This is disabled by default, as most users will not need it. However, if you're using a remote +# storage type without a messaging service setup, you may wish to set this to something like 3. +# - Set to -1 to disable the task completely. +sync-minutes = -1 + +# If the file watcher should be enabled. +# +# - When using a file-based storage type, LuckPerms can monitor the data files for changes, and +# automatically update when changes are detected. +# - If you don't want this feature to be active, set this option to false. +watch-files = true + +# Define which messaging service should be used by the plugin. +# +# - If enabled and configured, LuckPerms will use the messaging service to inform other connected +# servers of changes. +# - Use the command "/lp networksync" to manually push changes. +# - Data is NOT stored using this service. It is only used as a messaging platform. +# +# - If you decide to enable this feature, you should set "sync-minutes" to -1, as there is no need +# for LuckPerms to poll the database for changes. +# +# - Possible options: +# => sql Uses the SQL database to form a queue system for communication. Will only work when +# 'storage-method' is set to MySQL or MariaDB. This is chosen by default if the +# option is set to 'auto' and SQL storage is in use. Set to 'notsql' to disable this. +# => pluginmsg Uses the plugin messaging channels to communicate with the proxy. +# LuckPerms must be installed on your proxy & all connected servers backend servers. +# Won't work if you have more than one proxy. +# => redis Uses Redis pub-sub to push changes. Your server connection info must be configured +# below. +# => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. +# => auto Attempts to automatically setup a messaging service using redis or sql. +messaging-service = "auto" + +# If LuckPerms should automatically push updates after a change has been made with a command. +auto-push-updates = true + +# If LuckPerms should push logging entries to connected servers via the messaging service. +push-log-entries = true + +# If LuckPerms should broadcast received logging entries to players on this platform. +# +# - If you have LuckPerms installed on your backend servers as well as a BungeeCord proxy, you +# should set this option to false on either your backends or your proxies, to avoid players being +# messaged twice about log entries. +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" + username = "" + password = "" + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel { + enabled = false + master = "mymaster" + addresses = ["localhost:26379"] + username = "" + password = "" + } +} + +# Settings for nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats { + enabled = false + address = "localhost" + username = "" + password = "" + token = "" +} + +# Settings for RabbitMQ. +# Port 5672 is used by default; set address to "host:port" if differs +rabbitmq { + enabled = false + address = "localhost" + vhost = "/" + username = "guest" + password = "guest" +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | CUSTOMIZATION SETTINGS | # +# | | # +# | Settings that allow admins to customize the way LuckPerms operates. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls how temporary permissions/parents/meta should be accumulated. +# +# - The default behaviour is "deny". +# - This behaviour can also be specified when the command is executed. See the command usage +# documentation for more info. +# +# - Possible options: +# => accumulate durations will be added to the existing expiry time +# => replace durations will be replaced if the new duration is later than the current +# expiration +# => deny the command will just fail if you try to add another node with the same expiry +temporary-add-behaviour = "deny" + +# Controls how LuckPerms will determine a users "primary" group. +# +# - The meaning and influence of "primary groups" are explained in detail on the wiki. +# - The preferred approach is to let LuckPerms automatically determine a users primary group +# based on the relative weight of their parent groups. +# +# - Possible options: +# => stored use the value stored against the users record in the file/database +# => parents-by-weight just use the users most highly weighted parent +# => all-parents-by-weight same as above, but calculates based upon all parents inherited from +# both directly and indirectly +primary-group-calculation = "parents-by-weight" + +# If the plugin should check for "extra" permissions with users run LP commands. +# +# - These extra permissions allow finer control over what users can do with each command, and who +# they have access to edit. +# - The nature of the checks are documented on the wiki under "Argument based command permissions". +# - Argument based permissions are *not* static, unlike the 'base' permissions, and will depend upon +# the arguments given within the command. +argument-based-command-permissions = false + +# If the plugin should check whether senders are a member of a given group before they're able to +# edit the groups data or add/remove other users to/from it. +# Note: these limitations do not apply to the web editor! +require-sender-group-membership-to-modify = false + +# If the plugin should send log notifications to users whenever permissions are modified. +# +# - Notifications are only sent to those with the appropriate permission to receive them +# - They can also be temporarily enabled/disabled on a per-user basis using +# '/lp log notify ' +log-notify = true + +# Defines a list of log entries which should not be sent as notifications to users. +# +# - Each entry in the list is a RegEx expression which is matched against the log entry description. +log-notify-filtered-descriptions = [ +# "parent add example" +] + +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations = true + +# Defines the options for prefix and suffix stacking. +# +# - The feature allows you to display multiple prefixes or suffixes alongside a players username in +# chat. +# - It is explained and documented in more detail on the wiki under "Prefix & Suffix Stacking". +# +# - The options are divided into separate sections for prefixes and suffixes. +# - The 'duplicates' setting refers to how duplicate elements are handled. Can be 'retain-all', +# 'first-only' or 'last-only'. +# - The value of 'start-spacer' is included at the start of the resultant prefix/suffix. +# - The value of 'end-spacer' is included at the end of the resultant prefix/suffix. +# - The value of 'middle-spacer' is included between each element in the resultant prefix/suffix. +# +# - Possible format options: +# => highest Selects the value with the highest weight, from all values +# held by or inherited by the player. +# +# => lowest Same as above, except takes the one with the lowest weight. +# +# => highest_own Selects the value with the highest weight, but will not +# accept any inherited values. +# +# => lowest_own Same as above, except takes the value with the lowest weight. +# +# => highest_inherited Selects the value with the highest weight, but will only +# accept inherited values. +# +# => lowest_inherited Same as above, except takes the value with the lowest weight. +# +# => highest_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group on the given track. +# +# => lowest_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group not on the given track. +# +# => lowest_not_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_from_group_ Selects the value with the highest weight, but only if the +# value was inherited from the given group. +# +# => lowest_from_group_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_from_group_ Selects the value with the highest weight, but only if the +# value was not inherited from the given group. +# +# => lowest_not_from_group_ Same as above, except takes the value with the lowest weight. +meta-formatting { + prefix { + format = [ + "highest" + ] + duplicates = "first-only" + start-spacer = "" + middle-spacer = " " + end-spacer = "" + } + suffix { + format = [ + "highest" + ] + duplicates = "first-only" + start-spacer = "" + middle-spacer = " " + end-spacer = "" + } +} + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | PERMISSION CALCULATION AND INHERITANCE | # +# | | # +# | Modify the way permission checks, meta lookups and inheritance resolutions are handled. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The algorithm LuckPerms should use when traversing the "inheritance tree". +# +# - Possible options: +# => breadth-first See: https://en.wikipedia.org/wiki/Breadth-first_search +# => depth-first-pre-order See: https://en.wikipedia.org/wiki/Depth-first_search +# => depth-first-post-order See: https://en.wikipedia.org/wiki/Depth-first_search +inheritance-traversal-algorithm = "depth-first-pre-order" + +# If a final sort according to "inheritance rules" should be performed after the traversal algorithm +# has resolved the inheritance tree. +# +# "Inheritance rules" refers to things such as group weightings, primary group status, and the +# natural contextual ordering of the group nodes. +# +# Setting this to 'true' will allow for the inheritance rules to take priority over the structure of +# the inheritance tree. +# +# Effectively when this setting is 'true': the tree is flattened, and rules applied afterwards, +# and when this setting is 'false':, the rules are just applied during each step of the traversal. +post-traversal-inheritance-sort = false + +# Defines the mode used to determine whether a set of contexts are satisfied. +# +# - Possible options: +# => at-least-one-value-per-key Set A will be satisfied by another set B, if at least one of the +# key-value entries per key in A are also in B. +# => all-values-per-key Set A will be satisfied by another set B, if all key-value +# entries in A are also in B. +context-satisfy-mode = "at-least-one-value-per-key" + +# LuckPerms has a number of built-in contexts. These can be disabled by adding the context key to +# the list below. +disabled-contexts = [ +# "world" +] + +# +----------------------------------------------------------------------------------------------+ # +# | Permission resolution settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If users on this server should have their global permissions applied. +# When set to false, only server specific permissions will apply for users on this server +include-global = true + +# If users on this server should have their global world permissions applied. +# When set to false, only world specific permissions will apply for users on this server +include-global-world = true + +# If users on this server should have global (non-server specific) groups applied +apply-global-groups = true + +# If users on this server should have global (non-world specific) groups applied +apply-global-world-groups = true + +# +----------------------------------------------------------------------------------------------+ # +# | Meta lookup settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Defines how meta values should be selected. +# +# - Possible options: +# => inheritance Selects the meta value that was inherited first +# => highest-number Selects the highest numerical meta value +# => lowest-number Selects the lowest numerical meta value +meta-value-selection-default = "inheritance" + +# Defines how meta values should be selected per key. +meta-value-selection { + #max-homes = "highest-number" +} + +# +----------------------------------------------------------------------------------------------+ # +# | Inheritance settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If the plugin should apply wildcard permissions. +# +# - If set to true, LuckPerms will detect wildcard permissions, and resolve & apply all registered +# permissions matching the wildcard. +apply-wildcards = true + +# If LuckPerms should resolve and apply permissions according to the Sponge style implicit wildcard +# inheritance system. +# +# - That being: If a user has been granted "example", then the player should have also be +# automatically granted "example.function", "example.another", "example.deeper.nesting", +# and so on. +apply-sponge-implicit-wildcards = false + +# If the plugin should parse regex permissions. +# +# - If set to true, LuckPerms will detect regex permissions, marked with "r=" at the start of the +# node, and resolve & apply all registered permissions matching the regex. +apply-regex = true + +# If the plugin should complete and apply shorthand permissions. +# +# - If set to true, LuckPerms will detect and expand shorthand node patterns. +apply-shorthand = true + +# If the owner of an integrated server should bypass permission checks. +# +# - This setting only applies when LuckPerms is active on a single-player world. +# - The owner of an integrated server is the player whose client instance is running the server. +integrated-server-owner-bypasses-checks = true + +# +----------------------------------------------------------------------------------------------+ # +# | Extra settings | # +# +----------------------------------------------------------------------------------------------+ # + +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators = [] + +# Allows you to set "aliases" for the worlds sent forward for context calculation. +# +# - These aliases are provided in addition to the real world name. Applied recursively. +# - Remove the comment characters for the default aliases to apply. +world-rewrite { + #world_nether = "world" + #world_the_end = "world" +} + +# Define special group weights for this server. +# +# - Group weights can also be applied directly to group data, using the setweight command. +# - This section allows weights to be set on a per-server basis. +group-weight { + #admin = 10 +} + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | FINE TUNING OPTIONS | # +# | | # +# | A number of more niche settings for tweaking and changing behaviour. The section also | # +# | contains toggles for some more specialised features. It is only necessary to make changes to | # +# | these options if you want to fine-tune LuckPerms behaviour. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# +----------------------------------------------------------------------------------------------+ # +# | Server Operator (OP) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls whether server operators should exist at all. +# +# - When set to 'false', all players will be de-opped, and the /op and /deop commands will be +# disabled. Note that vanilla features like the spawn-protection require an operator on the +# server to work. +enable-ops = true + +# Enables or disables a special permission based system in LuckPerms for controlling OP status. +# +# - If set to true, any user with the permission "luckperms.autoop" will automatically be granted +# server operator status. This permission can be inherited, or set on specific servers/worlds, +# temporarily, etc. +# - Additionally, setting this to true will force the "enable-ops" option above to false. All users +# will be de-opped unless they have the permission node, and the op/deop commands will be +# disabled. +# - It is recommended that you use this option instead of assigning a single '*' permission. +# - However, on Forge this setting can be used as a "pseudo" root wildcard, as many mods support +# the operator system over permissions. +auto-op = false + +# +----------------------------------------------------------------------------------------------+ # +# | Miscellaneous (and rarely used) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If LuckPerms should produce extra logging output when it handles logins. +# +# - Useful if you're having issues with UUID forwarding or data not being loaded. +debug-logins = false + +# If LuckPerms should allow usernames with non alphanumeric characters. +# +# - Note that due to the design of the storage implementation, usernames must still be 16 characters +# or less. +allow-invalid-usernames = false + +# If LuckPerms should not require users to confirm bulkupdate operations. +# +# - When set to true, operations will be executed immediately. +# - This is not recommended, as bulkupdate has the potential to irreversibly delete large amounts of +# data, and is not designed to be executed automatically. +# - If automation is needed, users should prefer using the LuckPerms API. +skip-bulkupdate-confirmation = false + +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate = false + +# If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. +# +# - When this happens, the plugin will set their primary group back to default. +prevent-primary-group-removal = false + +# If LuckPerms should update the list of commands sent to the client when permissions are changed. +update-client-command-list = true + +# If LuckPerms should attempt to resolve Vanilla command target selectors for LP commands. +# See here for more info: https://minecraft.wiki/w/Target_selectors +resolve-command-selectors = false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode { + players = false + console = false +} + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands { + players = false + console = false +} diff --git a/nukkit/build.gradle b/nukkit/build.gradle index a8c3a3aa1..a30745eb1 100644 --- a/nukkit/build.gradle +++ b/nukkit/build.gradle @@ -1,21 +1,20 @@ plugins { - id 'com.github.johnrengelman.shadow' version '2.0.4' + alias(libs.plugins.shadow) } repositories { - mavenLocal() - maven { url 'https://repo.nukkitx.com/main/' } + maven { url 'https://repo.opencollab.dev/main/' } } dependencies { - compile project(':common') + implementation project(':common') compileOnly project(':common:loader-utils') compileOnly 'cn.nukkit:nukkit:1.0-SNAPSHOT' } shadowJar { - archiveName = 'luckperms-nukkit.jarinjar' + archiveFileName = 'luckperms-nukkit.jarinjar' dependencies { include(dependency('me.lucko.luckperms:.*')) @@ -35,6 +34,7 @@ shadowJar { relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' diff --git a/nukkit/loader/build.gradle b/nukkit/loader/build.gradle index a0dcb2bc6..8d0211ebd 100644 --- a/nukkit/loader/build.gradle +++ b/nukkit/loader/build.gradle @@ -1,28 +1,26 @@ plugins { - id 'com.github.johnrengelman.shadow' + alias(libs.plugins.shadow) } repositories { - mavenLocal() - maven { url 'https://repo.nukkitx.com/main/' } + maven { url 'https://repo.opencollab.dev/main/' } } dependencies { compileOnly 'cn.nukkit:nukkit:1.0-SNAPSHOT' - compile project(':api') - compile project(':common:loader-utils') + implementation project(':api') + implementation project(':common:loader-utils') } processResources { - from(sourceSets.main.resources.srcDirs) { + filesMatching('plugin.yml') { expand 'pluginVersion': project.ext.fullVersion - include 'plugin.yml' } } shadowJar { - archiveName = "LuckPerms-Nukkit-${project.ext.fullVersion}.jar" + archiveFileName = "LuckPerms-Nukkit-${project.ext.fullVersion}.jar" from { project(':nukkit').tasks.shadowJar.archiveFile @@ -31,4 +29,4 @@ shadowJar { artifacts { archives shadowJar -} \ No newline at end of file +} diff --git a/nukkit/loader/src/main/java/me/lucko/luckperms/nukkit/loader/NukkitLoaderPlugin.java b/nukkit/loader/src/main/java/me/lucko/luckperms/nukkit/loader/NukkitLoaderPlugin.java index 8f8cb1abc..e9efde91e 100644 --- a/nukkit/loader/src/main/java/me/lucko/luckperms/nukkit/loader/NukkitLoaderPlugin.java +++ b/nukkit/loader/src/main/java/me/lucko/luckperms/nukkit/loader/NukkitLoaderPlugin.java @@ -25,11 +25,10 @@ package me.lucko.luckperms.nukkit.loader; +import cn.nukkit.plugin.PluginBase; import me.lucko.luckperms.common.loader.JarInJarClassLoader; import me.lucko.luckperms.common.loader.LoaderBootstrap; -import cn.nukkit.plugin.PluginBase; - public class NukkitLoaderPlugin extends PluginBase { private static final String JAR_NAME = "luckperms-nukkit.jarinjar"; private static final String BOOTSTRAP_CLASS = "me.lucko.luckperms.nukkit.LPNukkitBootstrap"; diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitBootstrap.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitBootstrap.java index 497f1d020..bdac4e7ee 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitBootstrap.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitBootstrap.java @@ -25,18 +25,17 @@ package me.lucko.luckperms.nukkit; +import cn.nukkit.Player; +import cn.nukkit.Server; +import cn.nukkit.plugin.PluginBase; import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; import me.lucko.luckperms.common.plugin.logging.PluginLogger; - import net.luckperms.api.platform.Platform; -import cn.nukkit.Player; -import cn.nukkit.Server; -import cn.nukkit.plugin.PluginBase; - import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; @@ -49,7 +48,7 @@ /** * Bootstrap plugin for LuckPerms running on Nukkit. */ -public class LPNukkitBootstrap implements LuckPermsBootstrap, LoaderBootstrap { +public class LPNukkitBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { private final PluginBase loader; /** @@ -91,6 +90,7 @@ public LPNukkitBootstrap(PluginBase loader) { // provide adapters + @Override public PluginBase getLoader() { return this.loader; } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitPlugin.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitPlugin.java index d2c17533c..27ecf50bc 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitPlugin.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/LPNukkitPlugin.java @@ -25,6 +25,13 @@ package me.lucko.luckperms.nukkit; +import cn.nukkit.Player; +import cn.nukkit.command.PluginCommand; +import cn.nukkit.permission.Permission; +import cn.nukkit.plugin.PluginBase; +import cn.nukkit.plugin.PluginManager; +import cn.nukkit.plugin.service.ServicePriority; +import cn.nukkit.utils.Config; import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -39,8 +46,6 @@ import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.tasks.CacheHousekeepingTask; -import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; import me.lucko.luckperms.nukkit.calculator.NukkitCalculatorFactory; import me.lucko.luckperms.nukkit.context.NukkitContextManager; import me.lucko.luckperms.nukkit.context.NukkitPlayerCalculator; @@ -57,20 +62,10 @@ import me.lucko.luckperms.nukkit.listeners.NukkitAutoOpListener; import me.lucko.luckperms.nukkit.listeners.NukkitConnectionListener; import me.lucko.luckperms.nukkit.listeners.NukkitPlatformListener; - import net.luckperms.api.LuckPerms; import net.luckperms.api.query.QueryOptions; -import cn.nukkit.Player; -import cn.nukkit.command.PluginCommand; -import cn.nukkit.permission.Permission; -import cn.nukkit.plugin.PluginBase; -import cn.nukkit.plugin.PluginManager; -import cn.nukkit.plugin.service.ServicePriority; -import cn.nukkit.utils.Config; - import java.util.Optional; -import java.util.concurrent.TimeUnit; import java.util.stream.Stream; /** @@ -182,12 +177,6 @@ protected void registerApiOnPlatform(LuckPerms api) { this.bootstrap.getServer().getServiceManager().register(LuckPerms.class, api, this.bootstrap.getLoader(), ServicePriority.NORMAL); } - @Override - protected void registerHousekeepingTasks() { - this.bootstrap.getScheduler().asyncRepeating(new ExpireTemporaryTask(this), 3, TimeUnit.SECONDS); - this.bootstrap.getScheduler().asyncRepeating(new CacheHousekeepingTask(this), 2, TimeUnit.MINUTES); - } - @Override protected void performFinalSetup() { // register permissions diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitCommandExecutor.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitCommandExecutor.java index c4622f50c..f9a03dfad 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitCommandExecutor.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitCommandExecutor.java @@ -25,10 +25,6 @@ package me.lucko.luckperms.nukkit; -import me.lucko.luckperms.common.command.CommandManager; -import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; -import me.lucko.luckperms.common.sender.Sender; - import cn.nukkit.command.Command; import cn.nukkit.command.CommandExecutor; import cn.nukkit.command.CommandSender; @@ -37,6 +33,9 @@ import cn.nukkit.event.EventHandler; import cn.nukkit.event.Listener; import cn.nukkit.event.server.ServerCommandEvent; +import me.lucko.luckperms.common.command.CommandManager; +import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; +import me.lucko.luckperms.common.sender.Sender; import java.util.List; diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitConfigAdapter.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitConfigAdapter.java index e53937d4a..8e390caf3 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitConfigAdapter.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitConfigAdapter.java @@ -25,18 +25,15 @@ package me.lucko.luckperms.nukkit; -import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import cn.nukkit.utils.Config; import cn.nukkit.utils.ConfigSection; +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import java.io.File; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; public class NukkitConfigAdapter implements ConfigurationAdapter { private final LuckPermsPlugin plugin; @@ -75,17 +72,6 @@ public List getStringList(String path, List def) { return list == null ? def : list; } - @Override - public List getKeys(String path, List def) { - ConfigSection section = this.configuration.getSection(path); - if (section == null) { - return def; - } - - Set keys = section.getKeys(false); - return keys == null ? def : new ArrayList<>(keys); - } - @Override public Map getStringMap(String path, Map def) { Map map = new HashMap<>(); diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitEventBus.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitEventBus.java index aea3613c0..c901a89c9 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitEventBus.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitEventBus.java @@ -25,13 +25,12 @@ package me.lucko.luckperms.nukkit; -import me.lucko.luckperms.common.api.LuckPermsApiProvider; -import me.lucko.luckperms.common.event.AbstractEventBus; - import cn.nukkit.event.EventHandler; import cn.nukkit.event.Listener; import cn.nukkit.event.plugin.PluginDisableEvent; import cn.nukkit.plugin.Plugin; +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.event.AbstractEventBus; public class NukkitEventBus extends AbstractEventBus implements Listener { public NukkitEventBus(LPNukkitPlugin plugin, LuckPermsApiProvider apiProvider) { diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitPluginLogger.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitPluginLogger.java index 454814532..d5bcbe509 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitPluginLogger.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitPluginLogger.java @@ -25,9 +25,8 @@ package me.lucko.luckperms.nukkit; -import me.lucko.luckperms.common.plugin.logging.PluginLogger; - import cn.nukkit.utils.Logger; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; public class NukkitPluginLogger implements PluginLogger { private final Logger logger; diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitSchedulerAdapter.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitSchedulerAdapter.java index 8e33cb687..56b6a002a 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitSchedulerAdapter.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitSchedulerAdapter.java @@ -25,21 +25,27 @@ package me.lucko.luckperms.nukkit; -import me.lucko.luckperms.common.plugin.scheduler.AbstractJavaScheduler; +import me.lucko.luckperms.common.plugin.scheduler.JavaSchedulerAdapter; import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.common.sender.Sender; import java.util.concurrent.Executor; -public class NukkitSchedulerAdapter extends AbstractJavaScheduler implements SchedulerAdapter { - private final Executor sync; +public class NukkitSchedulerAdapter extends JavaSchedulerAdapter implements SchedulerAdapter { + private final Executor syncExecutor; public NukkitSchedulerAdapter(LPNukkitBootstrap bootstrap) { - this.sync = r -> bootstrap.getServer().getScheduler().scheduleTask(bootstrap.getLoader(), r, false); + super(bootstrap); + this.syncExecutor = r -> bootstrap.getServer().getScheduler().scheduleTask(bootstrap.getLoader(), r, false); + } + + public void executeSync(Runnable task) { + this.syncExecutor.execute(task); } @Override - public Executor sync() { - return this.sync; + public void executeSync(Sender ctx, Runnable task) { + this.syncExecutor.execute(task); } } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitSenderFactory.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitSenderFactory.java index dd2126b84..93ffb4b16 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitSenderFactory.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/NukkitSenderFactory.java @@ -25,18 +25,16 @@ package me.lucko.luckperms.nukkit; +import cn.nukkit.Player; +import cn.nukkit.command.CommandSender; +import cn.nukkit.command.ConsoleCommandSender; import me.lucko.luckperms.common.locale.TranslationManager; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.sender.SenderFactory; - import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.luckperms.api.util.Tristate; -import cn.nukkit.Player; -import cn.nukkit.command.CommandSender; -import cn.nukkit.command.ConsoleCommandSender; - import java.util.Locale; import java.util.UUID; diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/ChildProcessor.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/ChildProcessor.java index 75a2f63d0..4c91b4b87 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/ChildProcessor.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/ChildProcessor.java @@ -25,14 +25,14 @@ package me.lucko.luckperms.nukkit.calculator; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; import me.lucko.luckperms.nukkit.LPNukkitPlugin; - +import me.lucko.luckperms.nukkit.inject.server.LuckPermsPermissionMap; +import net.luckperms.api.node.Node; import net.luckperms.api.util.Tristate; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -44,11 +44,19 @@ public class ChildProcessor extends AbstractPermissionProcessor implements Permi private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(ChildProcessor.class); private final LPNukkitPlugin plugin; + private final Map sourceMap; + private final AtomicBoolean needsRefresh = new AtomicBoolean(false); - private Map childPermissions = Collections.emptyMap(); + private Map childPermissions; - public ChildProcessor(LPNukkitPlugin plugin) { + public ChildProcessor(LPNukkitPlugin plugin, Map sourceMap) { this.plugin = plugin; + this.sourceMap = sourceMap; + refresh(); + } + + private void refresh() { + this.childPermissions = processChildPermissions(this.sourceMap, this.plugin.getPermissionMap()); } @Override @@ -60,20 +68,18 @@ public TristateResult hasPermission(String permission) { } @Override - public void refresh() { + public void invalidate() { + this.needsRefresh.set(true); + } + + private static Map processChildPermissions(Map sourceMap, LuckPermsPermissionMap permissionMap) { Map childPermissions = new HashMap<>(); - this.sourceMap.forEach((key, value) -> { - Map children = this.plugin.getPermissionMap().getChildPermissions(key, value); + sourceMap.forEach((key, node) -> { + Map children = permissionMap.getChildPermissions(key, node.getValue()); children.forEach((childKey, childValue) -> { - childPermissions.put(childKey, RESULT_FACTORY.result(Tristate.of(childValue), "parent: " + key)); + childPermissions.put(childKey, RESULT_FACTORY.resultWithOverride(node, Tristate.of(childValue))); }); }); - this.childPermissions = childPermissions; - this.needsRefresh.set(false); - } - - @Override - public void invalidate() { - this.needsRefresh.set(true); + return childPermissions; } } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/DefaultPermissionMapProcessor.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/DefaultPermissionMapProcessor.java new file mode 100644 index 000000000..59f123544 --- /dev/null +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/DefaultPermissionMapProcessor.java @@ -0,0 +1,57 @@ +/* + * 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.nukkit.calculator; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.nukkit.LPNukkitPlugin; +import net.luckperms.api.util.Tristate; + +/** + * Permission Processor for Nukkits "default" permission system. + */ +public class DefaultPermissionMapProcessor extends AbstractPermissionProcessor implements PermissionProcessor { + private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(DefaultPermissionMapProcessor.class); + + private final LPNukkitPlugin plugin; + private final boolean isOp; + + public DefaultPermissionMapProcessor(LPNukkitPlugin plugin, boolean isOp) { + this.plugin = plugin; + this.isOp = isOp; + } + + @Override + public TristateResult hasPermission(String permission) { + Tristate t = this.plugin.getDefaultPermissionMap().lookupDefaultPermission(permission, this.isOp); + if (t != Tristate.UNDEFINED) { + return RESULT_FACTORY.result(t); + } + + return TristateResult.UNDEFINED; + } +} diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/DefaultsProcessor.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/DefaultsProcessor.java deleted file mode 100644 index b619f3c1b..000000000 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/DefaultsProcessor.java +++ /dev/null @@ -1,87 +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.nukkit.calculator; - -import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; -import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; -import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; -import me.lucko.luckperms.nukkit.LPNukkitPlugin; -import me.lucko.luckperms.nukkit.inject.PermissionDefault; - -import net.luckperms.api.util.Tristate; - -/** - * Permission Processor for Nukkits "default" permission system. - */ -public class DefaultsProcessor implements PermissionProcessor { - private static final TristateResult.Factory DEFAULT_PERMISSION_MAP_RESULT_FACTORY = new TristateResult.Factory(DefaultsProcessor.class, "default permission map"); - private static final TristateResult.Factory PERMISSION_MAP_RESULT_FACTORY = new TristateResult.Factory(DefaultsProcessor.class, "permission map"); - - private final LPNukkitPlugin plugin; - private final boolean overrideWildcards; - private final boolean isOp; - - public DefaultsProcessor(LPNukkitPlugin plugin, boolean overrideWildcards, boolean isOp) { - this.plugin = plugin; - this.overrideWildcards = overrideWildcards; - this.isOp = isOp; - } - - private boolean canOverrideWildcard(TristateResult prev) { - return this.overrideWildcards && - (prev.processorClass() == WildcardProcessor.class || prev.processorClass() == SpongeWildcardProcessor.class) && - prev.result() == Tristate.TRUE; - } - - @Override - public TristateResult hasPermission(TristateResult prev, String permission) { - if (prev != TristateResult.UNDEFINED) { - // Check to see if the result should be overridden - if (canOverrideWildcard(prev)) { - PermissionDefault def = PermissionDefault.fromPermission(this.plugin.getPermissionMap().get(permission)); - if (def != null) { - if (def == PermissionDefault.FALSE || this.isOp && def == PermissionDefault.NOT_OP) { - return PERMISSION_MAP_RESULT_FACTORY.result(Tristate.FALSE, "permission map (overriding wildcard): " + prev.cause()); - } - } - } - - return prev; - } - - Tristate t = this.plugin.getDefaultPermissionMap().lookupDefaultPermission(permission, this.isOp); - if (t != Tristate.UNDEFINED) { - return DEFAULT_PERMISSION_MAP_RESULT_FACTORY.result(t); - } - - PermissionDefault def = PermissionDefault.fromPermission(this.plugin.getPermissionMap().get(permission)); - if (def == null) { - return TristateResult.UNDEFINED; - } - return PERMISSION_MAP_RESULT_FACTORY.result(Tristate.of(def.getValue(this.isOp))); - } -} diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/NukkitCalculatorFactory.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/NukkitCalculatorFactory.java index 564282c3c..5bd15933d 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/NukkitCalculatorFactory.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/NukkitCalculatorFactory.java @@ -28,6 +28,7 @@ import me.lucko.luckperms.common.cacheddata.CacheMetadata; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorMonitored; import me.lucko.luckperms.common.calculator.processor.DirectProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import me.lucko.luckperms.common.calculator.processor.RegexProcessor; @@ -37,11 +38,12 @@ import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.nukkit.LPNukkitPlugin; import me.lucko.luckperms.nukkit.context.NukkitContextManager; - +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class NukkitCalculatorFactory implements CalculatorFactory { private final LPNukkitPlugin plugin; @@ -51,37 +53,38 @@ public NukkitCalculatorFactory(LPNukkitPlugin plugin) { } @Override - public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { - List processors = new ArrayList<>(7); + public PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata) { + List processors = new ArrayList<>(8); - processors.add(new DirectProcessor()); + processors.add(new DirectProcessor(sourceMap)); if (this.plugin.getConfiguration().get(ConfigKeys.APPLY_NUKKIT_CHILD_PERMISSIONS)) { - processors.add(new ChildProcessor(this.plugin)); + processors.add(new ChildProcessor(this.plugin, sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { - processors.add(new RegexProcessor()); + processors.add(new RegexProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { - processors.add(new WildcardProcessor()); + processors.add(new WildcardProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { - processors.add(new SpongeWildcardProcessor()); + processors.add(new SpongeWildcardProcessor(sourceMap)); } boolean op = queryOptions.option(NukkitContextManager.OP_OPTION).orElse(false); if (metadata.getHolderType() == HolderType.USER && this.plugin.getConfiguration().get(ConfigKeys.APPLY_NUKKIT_DEFAULT_PERMISSIONS)) { boolean overrideWildcards = this.plugin.getConfiguration().get(ConfigKeys.APPLY_DEFAULT_NEGATIONS_BEFORE_WILDCARDS); - processors.add(new DefaultsProcessor(this.plugin, overrideWildcards, op)); + processors.add(new DefaultPermissionMapProcessor(this.plugin, op)); + processors.add(new PermissionMapProcessor(this.plugin, overrideWildcards, op)); } if (op) { processors.add(OpProcessor.INSTANCE); } - return new PermissionCalculator(this.plugin, metadata, processors); + return new PermissionCalculatorMonitored(this.plugin, metadata, processors); } } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/OpProcessor.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/OpProcessor.java index de22d9575..e2570dff0 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/OpProcessor.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/OpProcessor.java @@ -25,16 +25,16 @@ package me.lucko.luckperms.nukkit.calculator; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractPermissionProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; - import net.luckperms.api.util.Tristate; /** * Permission Processor which is added for opped users, to simply return true if * no other processors match. */ -public final class OpProcessor implements PermissionProcessor { +public final class OpProcessor extends AbstractPermissionProcessor implements PermissionProcessor { private static final TristateResult TRUE_RESULT = new TristateResult.Factory(OpProcessor.class).result(Tristate.TRUE); public static final OpProcessor INSTANCE = new OpProcessor(); @@ -44,10 +44,7 @@ private OpProcessor() { } @Override - public TristateResult hasPermission(TristateResult prev, String permission) { - if (prev != TristateResult.UNDEFINED) { - return prev; - } + public TristateResult hasPermission(String permission) { return TRUE_RESULT; } } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/PermissionMapProcessor.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/PermissionMapProcessor.java new file mode 100644 index 000000000..9f0674371 --- /dev/null +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/calculator/PermissionMapProcessor.java @@ -0,0 +1,58 @@ +/* + * 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.nukkit.calculator; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractOverrideWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.nukkit.LPNukkitPlugin; +import me.lucko.luckperms.nukkit.inject.PermissionDefault; +import net.luckperms.api.util.Tristate; + +/** + * Permission Processor for Nukkits "default" permission system. + */ +public class PermissionMapProcessor extends AbstractOverrideWildcardProcessor implements PermissionProcessor { + private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(PermissionMapProcessor.class); + + private final LPNukkitPlugin plugin; + private final boolean isOp; + + public PermissionMapProcessor(LPNukkitPlugin plugin, boolean overrideWildcards, boolean isOp) { + super(overrideWildcards); + this.plugin = plugin; + this.isOp = isOp; + } + + @Override + public TristateResult hasPermission(String permission) { + PermissionDefault def = PermissionDefault.fromPermission(this.plugin.getPermissionMap().get(permission)); + if (def == null) { + return TristateResult.UNDEFINED; + } + return RESULT_FACTORY.result(Tristate.of(def.getValue(this.isOp))); + } +} diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/context/NukkitContextManager.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/context/NukkitContextManager.java index 9514862af..0ff92d4b5 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/context/NukkitContextManager.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/context/NukkitContextManager.java @@ -25,88 +25,46 @@ package me.lucko.luckperms.nukkit.context; -import com.github.benmanes.caffeine.cache.LoadingCache; - -import me.lucko.luckperms.common.cache.LoadingMap; -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.ContextManager; -import me.lucko.luckperms.common.context.QueryOptionsCache; -import me.lucko.luckperms.common.util.CaffeineFactory; +import cn.nukkit.Player; +import me.lucko.luckperms.common.context.manager.DetachedContextManager; +import me.lucko.luckperms.common.context.manager.QueryOptionsSupplier; import me.lucko.luckperms.nukkit.LPNukkitPlugin; - -import net.luckperms.api.context.ImmutableContextSet; +import me.lucko.luckperms.nukkit.inject.permissible.LuckPermsPermissible; +import me.lucko.luckperms.nukkit.inject.permissible.PermissibleInjector; import net.luckperms.api.query.OptionKey; import net.luckperms.api.query.QueryOptions; +import org.checkerframework.checker.nullness.qual.Nullable; -import cn.nukkit.Player; - +import java.util.Objects; import java.util.UUID; -import java.util.concurrent.TimeUnit; -public class NukkitContextManager extends ContextManager { +public class NukkitContextManager extends DetachedContextManager { public static final OptionKey OP_OPTION = OptionKey.of("op", Boolean.class); - // cache the creation of ContextsCache instances for online players with no expiry - private final LoadingMap> onlineSubjectCaches = LoadingMap.of(key -> new QueryOptionsCache<>(key, this)); - - // cache the creation of ContextsCache instances for offline players with a 1m expiry - private final LoadingCache> offlineSubjectCaches = CaffeineFactory.newBuilder() - .expireAfterAccess(1, TimeUnit.MINUTES) - .build(key -> { - QueryOptionsCache cache = this.onlineSubjectCaches.getIfPresent(key); - if (cache != null) { - return cache; - } - return new QueryOptionsCache<>(key, this); - }); - public NukkitContextManager(LPNukkitPlugin plugin) { super(plugin, Player.class, Player.class); } - public void onPlayerQuit(Player player) { - this.onlineSubjectCaches.remove(player); - } - @Override public UUID getUniqueId(Player player) { return player.getUniqueId(); } @Override - public QueryOptionsCache getCacheFor(Player subject) { - if (subject == null) { - throw new NullPointerException("subject"); - } - - if (subject.isOnline()) { - return this.onlineSubjectCaches.get(subject); - } else { - return this.offlineSubjectCaches.get(subject); + public @Nullable QueryOptionsSupplier getQueryOptionsSupplier(Player subject) { + Objects.requireNonNull(subject, "subject"); + LuckPermsPermissible permissible = PermissibleInjector.get(subject); + if (permissible != null) { + return permissible.getQueryOptionsSupplier(); } + return null; } @Override - protected void invalidateCache(Player subject) { - QueryOptionsCache cache = this.onlineSubjectCaches.getIfPresent(subject); - if (cache != null) { - cache.invalidate(); - } - - cache = this.offlineSubjectCaches.getIfPresent(subject); - if (cache != null) { - cache.invalidate(); - } - } - - @Override - public QueryOptions formQueryOptions(Player subject, ImmutableContextSet contextSet) { - QueryOptions.Builder queryOptions = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder(); + public void customizeQueryOptions(Player subject, QueryOptions.Builder builder) { if (subject.isOp()) { - queryOptions.option(OP_OPTION, true); + builder.option(OP_OPTION, true); } - - return queryOptions.context(contextSet).build(); } } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/context/NukkitPlayerCalculator.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/context/NukkitPlayerCalculator.java index c55e65626..b54c562a4 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/context/NukkitPlayerCalculator.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/context/NukkitPlayerCalculator.java @@ -25,28 +25,26 @@ package me.lucko.luckperms.nukkit.context; +import cn.nukkit.Player; +import cn.nukkit.Server; +import cn.nukkit.event.EventHandler; +import cn.nukkit.event.EventPriority; +import cn.nukkit.event.Listener; +import cn.nukkit.event.entity.EntityLevelChangeEvent; +import cn.nukkit.event.player.PlayerGameModeChangeEvent; +import cn.nukkit.level.Level; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.nukkit.LPNukkitPlugin; - import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextCalculator; import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; - import org.checkerframework.checker.nullness.qual.NonNull; -import cn.nukkit.Player; -import cn.nukkit.Server; -import cn.nukkit.event.EventHandler; -import cn.nukkit.event.EventPriority; -import cn.nukkit.event.Listener; -import cn.nukkit.event.entity.EntityLevelChangeEvent; -import cn.nukkit.event.player.PlayerGameModeChangeEvent; -import cn.nukkit.level.Level; - +import java.util.Locale; import java.util.Set; public class NukkitPlayerCalculator implements ContextCalculator, Listener { @@ -111,7 +109,7 @@ private static String getGamemodeName(int mode) { case Player.CREATIVE: return "creative"; case Player.ADVENTURE: return "adventure"; case Player.SPECTATOR: return "spectator"; - default: return Server.getGamemodeString(mode, true).toLowerCase(); + default: return Server.getGamemodeString(mode, true).toLowerCase(Locale.ROOT); } } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/PermissionDefault.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/PermissionDefault.java index da0a42d49..a3f184fcf 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/PermissionDefault.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/PermissionDefault.java @@ -25,11 +25,11 @@ package me.lucko.luckperms.nukkit.inject; -import org.checkerframework.checker.nullness.qual.Nullable; - import cn.nukkit.permission.Permission; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.HashMap; +import java.util.Locale; import java.util.Map; /** @@ -84,7 +84,7 @@ public boolean getValue(boolean op) { * @return Specified value, or null if not found */ public static @Nullable PermissionDefault getByName(String name) { - return LOOKUP.get(name.toLowerCase().replaceAll("[^a-z!]", "")); + return LOOKUP.get(name.toLowerCase(Locale.ROOT).replaceAll("[^a-z!]", "")); } public static @Nullable PermissionDefault fromPermission(@Nullable Permission permission) { diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/DummyPermissibleBase.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/DummyPermissibleBase.java index 8257c371a..1f67723c7 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/DummyPermissibleBase.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/DummyPermissibleBase.java @@ -25,13 +25,12 @@ package me.lucko.luckperms.nukkit.inject.permissible; -import me.lucko.luckperms.common.util.EmptyCollections; - import cn.nukkit.permission.PermissibleBase; import cn.nukkit.permission.Permission; import cn.nukkit.permission.PermissionAttachment; import cn.nukkit.permission.PermissionAttachmentInfo; import cn.nukkit.plugin.Plugin; +import me.lucko.luckperms.common.util.EmptyCollections; import java.lang.reflect.Field; import java.util.Collections; diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/LuckPermsPermissible.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/LuckPermsPermissible.java index 05b934770..f6d8f4455 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/LuckPermsPermissible.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/LuckPermsPermissible.java @@ -25,32 +25,28 @@ package me.lucko.luckperms.nukkit.inject.permissible; +import cn.nukkit.Player; +import cn.nukkit.permission.PermissibleBase; +import cn.nukkit.permission.Permission; +import cn.nukkit.permission.PermissionAttachment; +import cn.nukkit.permission.PermissionAttachmentInfo; +import cn.nukkit.plugin.Plugin; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; - -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.QueryOptionsCache; +import me.lucko.luckperms.common.context.manager.QueryOptionsSupplier; import me.lucko.luckperms.common.model.User; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import me.lucko.luckperms.nukkit.LPNukkitPlugin; -import me.lucko.luckperms.nukkit.calculator.DefaultsProcessor; import me.lucko.luckperms.nukkit.calculator.OpProcessor; +import me.lucko.luckperms.nukkit.calculator.PermissionMapProcessor; import me.lucko.luckperms.nukkit.inject.PermissionDefault; - import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import cn.nukkit.Player; -import cn.nukkit.permission.PermissibleBase; -import cn.nukkit.permission.Permission; -import cn.nukkit.permission.PermissionAttachment; -import cn.nukkit.permission.PermissionAttachmentInfo; -import cn.nukkit.plugin.Plugin; - import java.lang.reflect.Field; import java.util.Collection; import java.util.Iterator; @@ -97,7 +93,7 @@ public class LuckPermsPermissible extends PermissibleBase { private final LPNukkitPlugin plugin; // caches context lookups for the player - private final QueryOptionsCache queryOptionsSupplier; + private final QueryOptionsSupplier queryOptionsSupplier; // the players previous permissible. (the one they had before this one was injected) private PermissibleBase oldPermissible = null; @@ -114,7 +110,7 @@ public LuckPermsPermissible(Player player, User user, LPNukkitPlugin plugin) { this.user = Objects.requireNonNull(user, "user"); this.player = Objects.requireNonNull(player, "player"); this.plugin = Objects.requireNonNull(plugin, "plugin"); - this.queryOptionsSupplier = plugin.getContextManager().getCacheFor(player); + this.queryOptionsSupplier = plugin.getContextManager().createQueryOptionsSupplier(player); injectFakeAttachmentsList(); } @@ -144,13 +140,13 @@ public boolean isPermissionSet(String permission) { } QueryOptions queryOptions = this.queryOptionsSupplier.getQueryOptions(); - TristateResult result = this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission, PermissionCheckEvent.Origin.PLATFORM_LOOKUP_CHECK); + TristateResult result = this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET); if (result.result() == Tristate.UNDEFINED) { return false; } // ignore matches made from looking up in the permission map (replicate nukkit behaviour) - if (result.processorClass() == DefaultsProcessor.class && "permission map".equals(result.cause())) { + if (result.processorClass() == PermissionMapProcessor.class) { return false; } @@ -174,7 +170,7 @@ public boolean hasPermission(String permission) { } QueryOptions queryOptions = this.queryOptionsSupplier.getQueryOptions(); - return this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission, PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK).result().asBoolean(); + return this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result().asBoolean(); } @Override @@ -184,7 +180,7 @@ public boolean hasPermission(Permission permission) { } QueryOptions queryOptions = this.queryOptionsSupplier.getQueryOptions(); - TristateResult result = this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission.getName(), PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK); + TristateResult result = this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission.getName(), CheckOrigin.PLATFORM_API_HAS_PERMISSION); // override default op handling using the Permission class we have if (result.processorClass() == OpProcessor.class && this.plugin.getConfiguration().get(ConfigKeys.APPLY_BUKKIT_DEFAULT_PERMISSIONS)) { @@ -280,7 +276,7 @@ public void recalculatePermissions() { // the query options cache when op status changes. // (#invalidate is a fast call) if (this.queryOptionsSupplier != null) { // this method is called by the super class constructor, before this class has fully initialised - this.queryOptionsSupplier.invalidate(); + this.queryOptionsSupplier.invalidateCache(); } // but we don't need to do anything else in this method, unlike the Nukkit impl. @@ -303,6 +299,10 @@ public LPNukkitPlugin getPlugin() { return this.plugin; } + public QueryOptionsSupplier getQueryOptionsSupplier() { + return this.queryOptionsSupplier; + } + PermissibleBase getOldPermissible() { return this.oldPermissible; } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/LuckPermsPermissionAttachment.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/LuckPermsPermissionAttachment.java index 36e44ed42..65ed6055f 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/LuckPermsPermissionAttachment.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/LuckPermsPermissionAttachment.java @@ -25,28 +25,26 @@ package me.lucko.luckperms.nukkit.inject.permissible; +import cn.nukkit.permission.Permission; +import cn.nukkit.permission.PermissionAttachment; +import cn.nukkit.permission.PermissionRemovedExecutor; +import cn.nukkit.plugin.Plugin; import com.google.common.base.Preconditions; - import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.node.factory.NodeBuilders; - import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.NodeBuilder; import net.luckperms.api.node.metadata.NodeMetadataKey; import net.luckperms.api.query.Flag; import net.luckperms.api.query.QueryOptions; -import cn.nukkit.permission.Permission; -import cn.nukkit.permission.PermissionAttachment; -import cn.nukkit.permission.PermissionRemovedExecutor; -import cn.nukkit.plugin.Plugin; - import java.lang.reflect.Field; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -238,7 +236,7 @@ public void setPermission(String name, boolean value) { Objects.requireNonNull(name, "name is null"); Preconditions.checkArgument(!name.isEmpty(), "name is empty"); - String permission = name.toLowerCase(); + String permission = name.toLowerCase(Locale.ROOT); Boolean previous = this.perms.put(permission, value); if (previous != null && previous == value) { @@ -275,7 +273,7 @@ public void unsetPermission(String name, boolean value) { Objects.requireNonNull(name, "name is null"); Preconditions.checkArgument(!name.isEmpty(), "name is empty"); - String permission = name.toLowerCase(); + String permission = name.toLowerCase(Locale.ROOT); Boolean previous = this.perms.remove(permission); if (previous == null) { diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/MonitoredPermissibleBase.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/MonitoredPermissibleBase.java index d728d4014..2db971959 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/MonitoredPermissibleBase.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/MonitoredPermissibleBase.java @@ -25,20 +25,18 @@ package me.lucko.luckperms.nukkit.inject.permissible; -import me.lucko.luckperms.common.calculator.result.TristateResult; -import me.lucko.luckperms.common.plugin.LuckPermsPlugin; -import me.lucko.luckperms.common.query.QueryOptionsImpl; -import me.lucko.luckperms.common.verbose.VerboseCheckTarget; -import me.lucko.luckperms.common.verbose.VerboseHandler; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; - -import net.luckperms.api.util.Tristate; - import cn.nukkit.permission.PermissibleBase; import cn.nukkit.permission.Permission; import cn.nukkit.permission.PermissionAttachment; import cn.nukkit.permission.PermissionAttachmentInfo; import cn.nukkit.plugin.Plugin; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.query.QueryOptionsImpl; +import me.lucko.luckperms.common.verbose.VerboseCheckTarget; +import me.lucko.luckperms.common.verbose.VerboseHandler; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import net.luckperms.api.util.Tristate; import java.util.Map; @@ -68,8 +66,8 @@ public MonitoredPermissibleBase(LuckPermsPlugin plugin, PermissibleBase delegate this.initialised = true; } - private void logCheck(PermissionCheckEvent.Origin origin, String permission, boolean result) { - this.plugin.getVerboseHandler().offerPermissionCheckEvent(origin, this.verboseCheckTarget, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.of(Tristate.of(result))); + private void logCheck(CheckOrigin origin, String permission, boolean result) { + this.plugin.getVerboseHandler().offerPermissionCheckEvent(origin, this.verboseCheckTarget, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.forMonitoredResult(Tristate.of(result))); this.plugin.getPermissionRegistry().offer(permission); } @@ -84,7 +82,7 @@ public boolean isPermissionSet(String permission) { } final boolean result = this.delegate.isPermissionSet(permission); - logCheck(PermissionCheckEvent.Origin.PLATFORM_LOOKUP_CHECK, permission, result); + logCheck(CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET, permission, result); return result; } @@ -95,7 +93,7 @@ public boolean isPermissionSet(Permission permission) { } final boolean result = this.delegate.isPermissionSet(permission); - logCheck(PermissionCheckEvent.Origin.PLATFORM_LOOKUP_CHECK, permission.getName(), result); + logCheck(CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET, permission.getName(), result); return result; } @@ -106,7 +104,7 @@ public boolean hasPermission(String permission) { } final boolean result = this.delegate.hasPermission(permission); - logCheck(PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK, permission, result); + logCheck(CheckOrigin.PLATFORM_API_HAS_PERMISSION, permission, result); return result; } @@ -117,7 +115,7 @@ public boolean hasPermission(Permission permission) { } final boolean result = this.delegate.hasPermission(permission); - logCheck(PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK, permission.getName(), result); + logCheck(CheckOrigin.PLATFORM_API_HAS_PERMISSION, permission.getName(), result); return result; } 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..ca1c3f392 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 @@ -28,6 +28,7 @@ import cn.nukkit.Player; import cn.nukkit.permission.PermissibleBase; import cn.nukkit.permission.PermissionAttachment; +import org.checkerframework.checker.nullness.qual.Nullable; import java.lang.reflect.Field; import java.util.Set; @@ -82,7 +83,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); } @@ -141,4 +142,17 @@ public static void uninject(Player player, boolean dummy) throws Exception { } } + public static @Nullable LuckPermsPermissible get(Player player) { + PermissibleBase permissibleBase; + try { + permissibleBase = (PermissibleBase) PLAYER_PERMISSIBLE_FIELD.get(player); + } catch (IllegalAccessException e) { + return null; + } + if (permissibleBase instanceof LuckPermsPermissible) { + return (LuckPermsPermissible) permissibleBase; + } + return null; + } + } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/PermissibleMonitoringInjector.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/PermissibleMonitoringInjector.java index 604d313cb..ad95030bf 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/PermissibleMonitoringInjector.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/permissible/PermissibleMonitoringInjector.java @@ -25,10 +25,9 @@ package me.lucko.luckperms.nukkit.inject.permissible; -import me.lucko.luckperms.nukkit.LPNukkitPlugin; - import cn.nukkit.command.ConsoleCommandSender; import cn.nukkit.permission.PermissibleBase; +import me.lucko.luckperms.nukkit.LPNukkitPlugin; import java.lang.reflect.Field; import java.util.Objects; diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorDefaultsMap.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorDefaultsMap.java index a53e9063b..6fda212d0 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorDefaultsMap.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorDefaultsMap.java @@ -25,13 +25,11 @@ package me.lucko.luckperms.nukkit.inject.server; -import com.google.common.collect.ImmutableMap; - -import me.lucko.luckperms.nukkit.LPNukkitPlugin; - import cn.nukkit.Server; import cn.nukkit.permission.Permission; import cn.nukkit.plugin.PluginManager; +import com.google.common.collect.ImmutableMap; +import me.lucko.luckperms.nukkit.LPNukkitPlugin; import java.lang.reflect.Field; import java.util.HashMap; diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorPermissionMap.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorPermissionMap.java index 23c47914f..d78da28cf 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorPermissionMap.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorPermissionMap.java @@ -25,11 +25,10 @@ package me.lucko.luckperms.nukkit.inject.server; -import me.lucko.luckperms.nukkit.LPNukkitPlugin; - import cn.nukkit.Server; import cn.nukkit.permission.Permission; import cn.nukkit.plugin.PluginManager; +import me.lucko.luckperms.nukkit.LPNukkitPlugin; import java.lang.reflect.Field; import java.util.HashMap; diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorSubscriptionMap.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorSubscriptionMap.java index 691020dcb..17c240595 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorSubscriptionMap.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/InjectorSubscriptionMap.java @@ -25,11 +25,10 @@ package me.lucko.luckperms.nukkit.inject.server; -import me.lucko.luckperms.nukkit.LPNukkitPlugin; - import cn.nukkit.Server; import cn.nukkit.permission.Permissible; import cn.nukkit.plugin.PluginManager; +import me.lucko.luckperms.nukkit.LPNukkitPlugin; import java.lang.reflect.Field; import java.util.Map; diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/LuckPermsDefaultsMap.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/LuckPermsDefaultsMap.java index 5ca5af244..ff2ce74ea 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/LuckPermsDefaultsMap.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/LuckPermsDefaultsMap.java @@ -25,21 +25,18 @@ package me.lucko.luckperms.nukkit.inject.server; +import cn.nukkit.permission.Permission; +import cn.nukkit.plugin.PluginManager; import com.google.common.collect.ForwardingMap; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.cache.Cache; import me.lucko.luckperms.nukkit.LPNukkitPlugin; - import net.luckperms.api.util.Tristate; - import org.checkerframework.checker.nullness.qual.NonNull; -import cn.nukkit.permission.Permission; -import cn.nukkit.plugin.PluginManager; - import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -159,7 +156,7 @@ private final class DefaultsCache extends Cache> { protected @NonNull Map supply() { Map builder = new HashMap<>(); for (Permission perm : LuckPermsDefaultsMap.this.get(this.op).values()) { - String name = perm.getName().toLowerCase(); + String name = perm.getName().toLowerCase(Locale.ROOT); builder.put(name, true); for (Map.Entry child : LuckPermsDefaultsMap.this.plugin.getPermissionMap().getChildPermissions(name, true).entrySet()) { builder.putIfAbsent(child.getKey(), child.getValue()); diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/LuckPermsPermissionMap.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/LuckPermsPermissionMap.java index 316158a3b..a276b48c8 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/LuckPermsPermissionMap.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/inject/server/LuckPermsPermissionMap.java @@ -25,22 +25,20 @@ package me.lucko.luckperms.nukkit.inject.server; +import cn.nukkit.permission.Permission; +import cn.nukkit.plugin.PluginManager; import com.google.common.collect.ForwardingMap; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.cache.LoadingMap; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.treeview.PermissionRegistry; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import cn.nukkit.permission.Permission; -import cn.nukkit.plugin.PluginManager; - import java.lang.reflect.Field; import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -180,7 +178,7 @@ private void resolveChildren(Map accumulator, Map { public NukkitAutoOpListener(LPNukkitPlugin plugin) { - this.plugin = plugin; + super(plugin, plugin.getContextManager(), Player.class); } @Override - public void bind(EventBus bus) { - bus.subscribe(UserDataRecalculateEvent.class, this::onUserDataRecalculate); - bus.subscribe(ContextUpdateEvent.class, this::onContextUpdate); + protected boolean isServerAvailable() { + return true; } - private void onUserDataRecalculate(UserDataRecalculateEvent e) { - User user = ApiUser.cast(e.getUser()); - this.plugin.getBootstrap().getPlayer(user.getUniqueId()).ifPresent(this::refreshAutoOp); - } - - private void onContextUpdate(ContextUpdateEvent e) { - e.getSubject(Player.class).ifPresent(this::refreshAutoOp); + @Override + protected UUID getUniqueId(Player player) { + return player.getUniqueId(); } - private void refreshAutoOp(Player player) { - User user = this.plugin.getUserManager().getIfLoaded(player.getUniqueId()); - boolean value; - - if (user != null) { - QueryOptions queryOptions = this.plugin.getContextManager().getQueryOptions(player); - Map permData = user.getCachedData().getPermissionData(queryOptions).getPermissionMap(); - value = permData.getOrDefault(NODE, false); - } else { - value = false; - } - + @Override + protected void setOp(Player player, boolean value, boolean callerIsSync) { player.setOp(value); } 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 c6e3aa732..d6fce75e8 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 @@ -25,6 +25,13 @@ package me.lucko.luckperms.nukkit.listeners; +import cn.nukkit.Player; +import cn.nukkit.event.EventHandler; +import cn.nukkit.event.EventPriority; +import cn.nukkit.event.Listener; +import cn.nukkit.event.player.PlayerAsyncPreLoginEvent; +import cn.nukkit.event.player.PlayerLoginEvent; +import cn.nukkit.event.player.PlayerQuitEvent; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.locale.TranslationManager; @@ -33,18 +40,9 @@ import me.lucko.luckperms.nukkit.LPNukkitPlugin; import me.lucko.luckperms.nukkit.inject.permissible.LuckPermsPermissible; import me.lucko.luckperms.nukkit.inject.permissible.PermissibleInjector; - import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import cn.nukkit.Player; -import cn.nukkit.event.EventHandler; -import cn.nukkit.event.EventPriority; -import cn.nukkit.event.Listener; -import cn.nukkit.event.player.PlayerAsyncPreLoginEvent; -import cn.nukkit.event.player.PlayerLoginEvent; -import cn.nukkit.event.player.PlayerQuitEvent; - import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -195,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; } @@ -217,9 +215,6 @@ public void onPlayerQuit(PlayerQuitEvent e) { if (this.plugin.getConfiguration().get(ConfigKeys.AUTO_OP)) { player.setOp(false); } - - // remove their contexts cache - this.plugin.getContextManager().onPlayerQuit(player); }, 1, true); } diff --git a/nukkit/src/main/java/me/lucko/luckperms/nukkit/listeners/NukkitPlatformListener.java b/nukkit/src/main/java/me/lucko/luckperms/nukkit/listeners/NukkitPlatformListener.java index cbdc6b4da..1b6e2dd21 100644 --- a/nukkit/src/main/java/me/lucko/luckperms/nukkit/listeners/NukkitPlatformListener.java +++ b/nukkit/src/main/java/me/lucko/luckperms/nukkit/listeners/NukkitPlatformListener.java @@ -25,10 +25,6 @@ package me.lucko.luckperms.nukkit.listeners; -import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.locale.Message; -import me.lucko.luckperms.nukkit.LPNukkitPlugin; - import cn.nukkit.command.CommandSender; import cn.nukkit.event.Cancellable; import cn.nukkit.event.EventHandler; @@ -36,7 +32,11 @@ import cn.nukkit.event.player.PlayerCommandPreprocessEvent; import cn.nukkit.event.server.RemoteServerCommandEvent; import cn.nukkit.event.server.ServerCommandEvent; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.locale.Message; +import me.lucko.luckperms.nukkit.LPNukkitPlugin; +import java.util.Locale; import java.util.regex.Pattern; public class NukkitPlatformListener implements Listener { @@ -50,17 +50,17 @@ public NukkitPlatformListener(LPNukkitPlugin plugin) { @EventHandler(ignoreCancelled = true) public void onPlayerCommand(PlayerCommandPreprocessEvent e) { - handleCommand(e.getPlayer(), e.getMessage().toLowerCase(), e); + handleCommand(e.getPlayer(), e.getMessage().toLowerCase(Locale.ROOT), e); } @EventHandler(ignoreCancelled = true) public void onServerCommand(ServerCommandEvent e) { - handleCommand(e.getSender(), e.getCommand().toLowerCase(), e); + handleCommand(e.getSender(), e.getCommand().toLowerCase(Locale.ROOT), e); } @EventHandler(ignoreCancelled = true) public void onRemoteServerCommand(RemoteServerCommandEvent e) { - handleCommand(e.getSender(), e.getCommand().toLowerCase(), e); + handleCommand(e.getSender(), e.getCommand().toLowerCase(Locale.ROOT), e); } private void handleCommand(CommandSender sender, String cmdLine, Cancellable event) { diff --git a/nukkit/src/main/resources/config.yml b/nukkit/src/main/resources/config.yml index efd7686ff..434c781a1 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 | # @@ -154,15 +154,25 @@ data: #verifyServerCertificate: false # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). # - Change this if you want to use different tables for different servers. table-prefix: 'luckperms_' - # The prefix to use for all LuckPerms collections. Change this if you want to use different - # collections for different servers. The default is no prefix. + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. mongodb-collection-prefix: '' - # MongoDB ClientConnectionURI for use with replica sets and custom connection options - # - See https://docs.mongodb.com/manual/reference/connection-string/ + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ mongodb-connection-uri: '' # Define settings for a "split" storage setup. @@ -226,6 +236,9 @@ watch-files: true # below. # => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be # configured below. +# => nats Uses Nats pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. # => auto Attempts to automatically setup a messaging service using redis or sql. messaging-service: auto @@ -244,10 +257,31 @@ 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 + username: '' password: '' + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel: + enabled: false + master: mymaster + addresses: + - localhost:26379 + username: '' + password: '' + +# Settings for Nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats: + enabled: false + address: localhost + username: '' + password: '' + token: '' # Settings for RabbitMQ. # Port 5672 is used by default; set address to "host:port" if differs @@ -535,6 +569,13 @@ apply-nukkit-attachment-permissions: true # | Extra settings | # # +----------------------------------------------------------------------------------------------+ # +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators: [] + # Allows you to set "aliases" for the worlds sent forward for context calculation. # # - These aliases are provided in addition to the real world name. Applied recursively. @@ -570,7 +611,8 @@ group-weight: # Controls whether server operators should exist at all. # # - When set to 'false', all players will be de-opped, and the /op and /deop commands will be -# disabled. +# disabled. Note that vanilla features like the spawn-protection require an operator on the +# server to work. enable-ops: true # Enables or disables a special permission based system in LuckPerms for controlling OP status. @@ -612,7 +654,43 @@ allow-invalid-usernames: true # - If automation is needed, users should prefer using the LuckPerms API. skip-bulkupdate-confirmation: false +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate: false + # If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. # # - When this happens, the plugin will set their primary group back to default. prevent-primary-group-removal: false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode: + players: false + console: false + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands: + players: false + console: false diff --git a/settings.gradle b/settings.gradle index 37f938678..6c3dd0ec1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,19 +1,28 @@ -// Fabric Needs this pluginManagement { repositories { - jcenter() + gradlePluginPortal() maven { - url 'https://maven.fabricmc.net/' + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + maven { + name = 'Forge' + url = 'https://maven.minecraftforge.net/' } - gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version("1.0.0") +} + rootProject.name = 'luckperms' include ( 'api', 'common', 'common:loader-utils', + 'common:minecraft', + 'common:placeholders', 'bukkit', 'bukkit:loader', 'bukkit-legacy', @@ -21,8 +30,21 @@ include ( 'bungee', 'bungee:loader', 'fabric', + 'neoforge', + 'neoforge:loader', + 'forge', + 'forge:loader', 'nukkit', 'nukkit:loader', - 'sponge', 'sponge:sponge-service', 'sponge:sponge-service-api6', 'sponge:sponge-service-api7', - 'velocity' + 'hytale', + 'hytale:loader', + 'hytale:loader-with-deps', + 'sponge', + 'sponge:loader', + 'sponge:sponge-service', + 'sponge:sponge-service-proxy', + 'velocity', + 'standalone', + 'standalone:loader', + 'standalone:app' ) diff --git a/sponge/build.gradle b/sponge/build.gradle index 76d45f106..8d7b2556d 100644 --- a/sponge/build.gradle +++ b/sponge/build.gradle @@ -1,46 +1,44 @@ plugins { - id 'net.kyori.blossom' version '1.1.0' - id 'com.github.johnrengelman.shadow' version '2.0.4' + alias(libs.plugins.shadow) +} + +tasks.withType(JavaCompile).configureEach { + options.release = 21 } repositories { - maven { url 'https://repo.spongepowered.org/maven' } + maven { url 'https://repo.spongepowered.org/repository/maven-public/' } } dependencies { - compile project(':common') - compile project(':sponge:sponge-service') - compile project(':sponge:sponge-service-api6') - compile project(':sponge:sponge-service-api7') + implementation project(':common') + implementation project(':sponge:sponge-service') + implementation project(':sponge:sponge-service-proxy') + compileOnly project(':common:loader-utils') - compileOnly('org.spongepowered:spongeapi:7.3.0') { - exclude(module: 'configurate-core') - exclude(module: 'configurate-hocon') - exclude(module: 'configurate-gson') - exclude(module: 'configurate-yaml') - } - annotationProcessor('org.spongepowered:spongeapi:7.3.0') { + compileOnly('org.spongepowered:spongeapi:12.0.0') { exclude(module: 'configurate-core') exclude(module: 'configurate-hocon') exclude(module: 'configurate-gson') exclude(module: 'configurate-yaml') } + compileOnly 'com.google.guava:guava:33.3.1-jre' } -blossom { - replaceTokenIn('src/main/java/me/lucko/luckperms/sponge/LPSpongeBootstrap.java') - replaceToken '@version@', project.ext.fullVersion +processResources { + filesMatching('META-INF/sponge_plugins.json') { + expand 'pluginVersion': project.ext.fullVersion + } } shadowJar { - archiveName = "LuckPerms-Sponge-${project.ext.fullVersion}.jar" + archiveFileName = 'luckperms-sponge.jarinjar' dependencies { - include(dependency('net.luckperms:.*')) include(dependency('me.lucko.luckperms:.*')) } - relocate 'net.kyori.adventure', 'me.lucko.luckperms.lib.adventure' + //relocate 'net.kyori.adventure', 'me.lucko.luckperms.lib.adventure' relocate 'net.kyori.event', 'me.lucko.luckperms.lib.eventbus' relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine' relocate 'okio', 'me.lucko.luckperms.lib.okio' @@ -54,6 +52,7 @@ shadowJar { relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' diff --git a/sponge/loader/build.gradle b/sponge/loader/build.gradle new file mode 100644 index 000000000..796861070 --- /dev/null +++ b/sponge/loader/build.gradle @@ -0,0 +1,36 @@ +plugins { + alias(libs.plugins.shadow) +} + +tasks.withType(JavaCompile).configureEach { + options.release = 21 +} + +repositories { + maven { url 'https://repo.spongepowered.org/repository/maven-public/' } +} + +dependencies { + compileOnly 'org.spongepowered:spongeapi:12.0.0' + + implementation project(':api') + implementation project(':common:loader-utils') +} + +processResources { + filesMatching('META-INF/sponge_plugins.json') { + expand 'pluginVersion': project.ext.fullVersion + } +} + +shadowJar { + archiveFileName = "LuckPerms-Sponge-${project.ext.fullVersion}.jar" + + from { + project(':sponge').tasks.shadowJar.archiveFile + } +} + +artifacts { + archives shadowJar +} \ No newline at end of file diff --git a/sponge/loader/src/main/java/me/lucko/luckperms/sponge/loader/SpongeLoaderPlugin.java b/sponge/loader/src/main/java/me/lucko/luckperms/sponge/loader/SpongeLoaderPlugin.java new file mode 100644 index 000000000..233ca6f00 --- /dev/null +++ b/sponge/loader/src/main/java/me/lucko/luckperms/sponge/loader/SpongeLoaderPlugin.java @@ -0,0 +1,73 @@ +/* + * 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.sponge.loader; + +import com.google.inject.Inject; +import com.google.inject.Injector; +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import org.spongepowered.api.Server; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.Order; +import org.spongepowered.api.event.lifecycle.ConstructPluginEvent; +import org.spongepowered.api.event.lifecycle.StoppingEngineEvent; +import org.spongepowered.plugin.builtin.jvm.Plugin; + +import java.util.function.Supplier; + +@Plugin("luckperms") +public class SpongeLoaderPlugin implements Supplier { + private static final String JAR_NAME = "luckperms-sponge.jarinjar"; + private static final String BOOTSTRAP_CLASS = "me.lucko.luckperms.sponge.LPSpongeBootstrap"; + + private final LoaderBootstrap plugin; + private final Injector injector; + + @Inject + public SpongeLoaderPlugin(Injector injector) { + this.injector = injector; + + JarInJarClassLoader loader = new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME); + this.plugin = loader.instantiatePlugin(BOOTSTRAP_CLASS, Supplier.class, this); + } + + @Override + public Injector get() { + return this.injector; + } + + @Listener(order = Order.FIRST) + public void onEnable(ConstructPluginEvent event) { + this.plugin.onLoad(); + this.plugin.onEnable(); + } + + @Listener + public void onDisable(StoppingEngineEvent event) { + this.plugin.onDisable(); + } + +} diff --git a/sponge/loader/src/main/resources/META-INF/sponge_plugins.json b/sponge/loader/src/main/resources/META-INF/sponge_plugins.json new file mode 100644 index 000000000..3945b53fb --- /dev/null +++ b/sponge/loader/src/main/resources/META-INF/sponge_plugins.json @@ -0,0 +1,31 @@ +{ + "loader": { + "name": "java_plain", + "version": "1.0" + }, + "license": "MIT", + "plugins": [ + { + "id": "luckperms", + "name": "LuckPerms", + "version": "${pluginVersion}", + "entrypoint": "me.lucko.luckperms.sponge.loader.SpongeLoaderPlugin", + "description": "A permissions plugin", + "links": { + "homepage": "https://luckperms.net" + }, + "contributors": [ + { + "name": "Luck", + "description": "Developer" + } + ], + "dependencies": [ + { + "id": "spongeapi", + "version": "12.0.0" + } + ] + } + ] +} \ No newline at end of file diff --git a/sponge/sponge-service-api6/build.gradle b/sponge/sponge-service-api6/build.gradle deleted file mode 100644 index e714b63aa..000000000 --- a/sponge/sponge-service-api6/build.gradle +++ /dev/null @@ -1,15 +0,0 @@ -repositories { - maven { url 'https://repo.spongepowered.org/maven' } -} - -dependencies { - compile project(':common') - compile project(':sponge:sponge-service') - - compileOnly('org.spongepowered:spongeapi:6.0.0') { - exclude(module: 'configurate-core') - exclude(module: 'configurate-hocon') - exclude(module: 'configurate-gson') - exclude(module: 'configurate-yaml') - } -} diff --git a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/DescriptionBuilder.java b/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/DescriptionBuilder.java deleted file mode 100644 index f1c4c8bf3..000000000 --- a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/DescriptionBuilder.java +++ /dev/null @@ -1,142 +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.sponge.service.proxy.api6; - -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; -import me.lucko.luckperms.sponge.service.model.LPPermissionDescription; -import me.lucko.luckperms.sponge.service.model.LPPermissionService; -import me.lucko.luckperms.sponge.service.model.LPSubject; -import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - -import net.luckperms.api.util.Tristate; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.spongepowered.api.plugin.PluginContainer; -import org.spongepowered.api.service.permission.PermissionDescription; -import org.spongepowered.api.service.permission.PermissionService; -import org.spongepowered.api.text.Text; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -public final class DescriptionBuilder implements PermissionDescription.Builder, ProxiedServiceObject { - - public static LPPermissionDescription registerDescription(LPPermissionService service, PermissionDescription description) { - //noinspection ConstantConditions - if (description.getOwner() == null) { - return null; - } - return service.registerPermissionDescription(description.getId(), description.getDescription(), description.getOwner()); - } - - private final @NonNull LPPermissionService service; - private final @NonNull PluginContainer container; - private final @NonNull Map roles = new HashMap<>(); - private @Nullable String id = null; - private @Nullable Text description = null; - - public DescriptionBuilder(@NonNull LPPermissionService service, @NonNull PluginContainer container) { - this.service = Objects.requireNonNull(service, "service"); - this.container = Objects.requireNonNull(container, "container"); - } - - @Override - public PermissionDescription.@NonNull Builder id(@NonNull String id) { - this.id = Objects.requireNonNull(id, "id"); - return this; - } - - @Override - public PermissionDescription.@NonNull Builder description(@NonNull Text description) { - this.description = Objects.requireNonNull(description, "description"); - return this; - } - - @Override - public PermissionDescription.@NonNull Builder assign(@NonNull String role, boolean value) { - Objects.requireNonNull(role, "role"); - this.roles.put(role, Tristate.of(value)); - return this; - } - - @Override - public @NonNull PermissionDescription register() throws IllegalStateException { - if (this.id == null) { - throw new IllegalStateException("id cannot be null"); - } - - LPPermissionDescription description = this.service.registerPermissionDescription(this.id, this.description, this.container); - - // Set role-templates - LPSubjectCollection subjects = this.service.getCollection(PermissionService.SUBJECTS_ROLE_TEMPLATE); - for (Map.Entry assignment : this.roles.entrySet()) { - LPSubject roleSubject = subjects.loadSubject(assignment.getKey()).join(); - roleSubject.getTransientSubjectData().setPermission(ImmutableContextSetImpl.EMPTY, this.id, assignment.getValue()); - } - - // null stuff so this instance can be reused - this.roles.clear(); - this.id = null; - this.description = null; - - return description.sponge(); - } - - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (!(o instanceof DescriptionBuilder)) return false; - final DescriptionBuilder other = (DescriptionBuilder) o; - - return this.container.equals(other.container) && - this.roles.equals(other.roles) && - Objects.equals(this.id, other.id) && - Objects.equals(this.description, other.description); - } - - @Override - public int hashCode() { - final int PRIME = 59; - int result = 1; - result = result * PRIME + this.container.hashCode(); - result = result * PRIME + this.roles.hashCode(); - result = result * PRIME + (this.id == null ? 43 : this.id.hashCode()); - result = result * PRIME + (this.description == null ? 43 : this.description.hashCode()); - return result; - } - - @Override - public String toString() { - return "SimpleDescriptionBuilder(" + - "container=" + this.container + ", " + - "roles=" + this.roles + ", " + - "id=" + this.id + ", " + - "description=" + this.description + ")"; - } -} diff --git a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/PermissionDescriptionProxy.java b/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/PermissionDescriptionProxy.java deleted file mode 100644 index e59f41c7d..000000000 --- a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/PermissionDescriptionProxy.java +++ /dev/null @@ -1,89 +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.sponge.service.proxy.api6; - -import me.lucko.luckperms.common.util.ImmutableCollectors; -import me.lucko.luckperms.sponge.service.model.LPPermissionDescription; -import me.lucko.luckperms.sponge.service.model.LPPermissionService; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.Sponge; -import org.spongepowered.api.plugin.PluginContainer; -import org.spongepowered.api.service.permission.PermissionDescription; -import org.spongepowered.api.service.permission.Subject; -import org.spongepowered.api.text.Text; - -import java.util.Map; - -public final class PermissionDescriptionProxy implements PermissionDescription, ProxiedServiceObject { - private final LPPermissionService service; - private final LPPermissionDescription handle; - - public PermissionDescriptionProxy(LPPermissionService service, LPPermissionDescription handle) { - this.service = service; - this.handle = handle; - } - - @Override - public @NonNull String getId() { - return this.handle.getId(); - } - - @Override - public @NonNull Text getDescription() { - return this.handle.getDescription().orElse(Text.EMPTY); - } - - @Override - public @NonNull PluginContainer getOwner() { - return this.handle.getOwner().orElseGet(() -> Sponge.getGame().getPluginManager().fromInstance(this.service.getPlugin()).orElseThrow(() -> new RuntimeException("Unable to get LuckPerms instance."))); - } - - @Override - public @NonNull Map getAssignedSubjects(@NonNull String s) { - return this.handle.getAssignedSubjects(s).entrySet().stream() - .collect(ImmutableCollectors.toMap( - e -> new SubjectProxy(this.service, e.getKey().toReference()), - Map.Entry::getValue - )); - } - - @Override - public boolean equals(Object o) { - return o == this || o instanceof PermissionDescriptionProxy && this.handle.equals(((PermissionDescriptionProxy) o).handle); - } - - @Override - public int hashCode() { - return this.handle.hashCode(); - } - - @Override - public String toString() { - return "luckperms.api6.PermissionDescriptionProxy(handle=" + this.handle + ")"; - } -} diff --git a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/PermissionServiceProxy.java b/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/PermissionServiceProxy.java deleted file mode 100644 index 18e278ec8..000000000 --- a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/PermissionServiceProxy.java +++ /dev/null @@ -1,121 +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.sponge.service.proxy.api6; - -import me.lucko.luckperms.common.util.ImmutableCollectors; -import me.lucko.luckperms.sponge.service.model.LPPermissionDescription; -import me.lucko.luckperms.sponge.service.model.LPPermissionService; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.Sponge; -import org.spongepowered.api.plugin.PluginContainer; -import org.spongepowered.api.service.context.ContextCalculator; -import org.spongepowered.api.service.permission.PermissionDescription; -import org.spongepowered.api.service.permission.PermissionService; -import org.spongepowered.api.service.permission.Subject; -import org.spongepowered.api.service.permission.SubjectCollection; - -import java.util.Collection; -import java.util.Map; -import java.util.Optional; - -public final class PermissionServiceProxy implements PermissionService, ProxiedServiceObject { - private final LPPermissionService handle; - - public PermissionServiceProxy(LPPermissionService handle) { - this.handle = handle; - } - - @Override - public @NonNull SubjectCollection getUserSubjects() { - return this.handle.getUserSubjects().sponge(); - } - - @Override - public @NonNull SubjectCollection getGroupSubjects() { - return this.handle.getGroupSubjects().sponge(); - } - - @Override - public @NonNull Subject getDefaults() { - return this.handle.getRootDefaults().sponge(); - } - - @Override - public @NonNull SubjectCollection getSubjects(@NonNull String s) { - return this.handle.getCollection(s).sponge(); - } - - @Override - public @NonNull Map getKnownSubjects() { - return this.handle.getLoadedCollections().entrySet().stream() - .collect(ImmutableCollectors.toMap( - Map.Entry::getKey, - e -> e.getValue().sponge() - )); - } - - @Override - public Optional newDescriptionBuilder(@NonNull Object o) { - Optional container = Sponge.getGame().getPluginManager().fromInstance(o); - if (!container.isPresent()) { - throw new IllegalArgumentException("Couldn't find a plugin container for " + o.getClass().getSimpleName()); - } - - return Optional.of(new DescriptionBuilder(this.handle, container.get())); - } - - @Override - public @NonNull Optional getDescription(@NonNull String s) { - return this.handle.getDescription(s).map(LPPermissionDescription::sponge); - } - - @Override - public @NonNull Collection getDescriptions() { - return this.handle.getDescriptions().stream().map(LPPermissionDescription::sponge).collect(ImmutableCollectors.toSet()); - } - - @Override - public void registerContextCalculator(@NonNull ContextCalculator contextCalculator) { - this.handle.registerContextCalculator(contextCalculator); - } - - @Override - public boolean equals(Object o) { - return o == this || o instanceof PermissionServiceProxy && this.handle.equals(((PermissionServiceProxy) o).handle); - } - - @Override - public int hashCode() { - return this.handle.hashCode(); - } - - @Override - public String toString() { - return "luckperms.api6.PermissionServiceProxy(handle=" + this.handle + ")"; - } -} diff --git a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/SubjectCollectionProxy.java b/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/SubjectCollectionProxy.java deleted file mode 100644 index b3133a715..000000000 --- a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/SubjectCollectionProxy.java +++ /dev/null @@ -1,128 +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.sponge.service.proxy.api6; - -import me.lucko.luckperms.common.util.ImmutableCollectors; -import me.lucko.luckperms.sponge.service.CompatibilityUtil; -import me.lucko.luckperms.sponge.service.model.LPPermissionService; -import me.lucko.luckperms.sponge.service.model.LPSubject; -import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.service.context.Context; -import org.spongepowered.api.service.permission.Subject; -import org.spongepowered.api.service.permission.SubjectCollection; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -@SuppressWarnings("unchecked") -public final class SubjectCollectionProxy implements SubjectCollection, ProxiedServiceObject { - private final LPPermissionService service; - private final LPSubjectCollection handle; - - public SubjectCollectionProxy(LPPermissionService service, LPSubjectCollection handle) { - this.service = service; - this.handle = handle; - } - - @Override - public @NonNull String getIdentifier() { - return this.handle.getIdentifier(); - } - - @Override - public @NonNull Subject get(@NonNull String s) { - // force load the subject. - // after this call, users will expect that the subject is loaded in memory. - return this.handle.loadSubject(s).thenApply(LPSubject::sponge).join(); - } - - @Override - public boolean hasRegistered(@NonNull String s) { - return this.handle.hasRegistered(s).join(); - } - - @SuppressWarnings("rawtypes") - @Override - public @NonNull Iterable getAllSubjects() { - // this will lazily load all subjects. it will initially just get the identifiers of each subject, and will initialize dummy - // providers for those identifiers. when any methods against the dummy are called, the actual data will be loaded. - // this behaviour should be replaced when CompletableFutures are added to Sponge - return (List) this.handle.getAllIdentifiers() - .thenApply(ids -> ids.stream() - .map(s -> new SubjectProxy(this.service, this.service.getReferenceFactory().obtain(getIdentifier(), s))) - .collect(ImmutableCollectors.toList()) - ).join(); - } - - @SuppressWarnings("rawtypes") - @Override - public @NonNull Map getAllWithPermission(@NonNull String s) { - // again, these methods will lazily load subjects. - return (Map) this.handle.getAllWithPermission(s) - .thenApply(map -> map.entrySet().stream() - .collect(ImmutableCollectors.toMap( - e -> new SubjectProxy(this.service, e.getKey()), - Map.Entry::getValue - )) - ).join(); - } - - @SuppressWarnings("rawtypes") - @Override - public @NonNull Map getAllWithPermission(@NonNull Set set, @NonNull String s) { - return (Map) this.handle.getAllWithPermission(CompatibilityUtil.convertContexts(set), s) - .thenApply(map -> map.entrySet().stream() - .collect(ImmutableCollectors.toMap( - e -> new SubjectProxy(this.service, e.getKey()), - Map.Entry::getValue - )) - ).join(); - } - - @Override - public @NonNull Subject getDefaults() { - return this.handle.getDefaults().sponge(); - } - - @Override - public boolean equals(Object o) { - return o == this || o instanceof SubjectCollectionProxy && this.handle.equals(((SubjectCollectionProxy) o).handle); - } - - @Override - public int hashCode() { - return this.handle.hashCode(); - } - - @Override - public String toString() { - return "luckperms.api6.SubjectCollectionProxy(handle=" + this.handle + ")"; - } -} diff --git a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/SubjectDataProxy.java b/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/SubjectDataProxy.java deleted file mode 100644 index 4451b0531..000000000 --- a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/SubjectDataProxy.java +++ /dev/null @@ -1,211 +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.sponge.service.proxy.api6; - -import me.lucko.luckperms.common.util.ImmutableCollectors; -import me.lucko.luckperms.sponge.service.CompatibilityUtil; -import me.lucko.luckperms.sponge.service.model.LPPermissionService; -import me.lucko.luckperms.sponge.service.model.LPSubject; -import me.lucko.luckperms.sponge.service.model.LPSubjectData; -import me.lucko.luckperms.sponge.service.model.LPSubjectReference; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.service.context.Context; -import org.spongepowered.api.service.permission.Subject; -import org.spongepowered.api.service.permission.SubjectData; -import org.spongepowered.api.util.Tristate; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; - -@SuppressWarnings("unchecked") -public final class SubjectDataProxy implements SubjectData, ProxiedServiceObject { - private final LPPermissionService service; - private final LPSubjectReference ref; - private final boolean enduring; - - public SubjectDataProxy(LPPermissionService service, LPSubjectReference ref, boolean enduring) { - this.service = service; - this.ref = ref; - this.enduring = enduring; - } - - private CompletableFuture handle() { - return this.enduring ? - this.ref.resolveLp().thenApply(LPSubject::getSubjectData) : - this.ref.resolveLp().thenApply(LPSubject::getTransientSubjectData); - } - - @SuppressWarnings("rawtypes") - @Override - public @NonNull Map, Map> getAllPermissions() { - return (Map) handle().thenApply(handle -> handle.getAllPermissions().entrySet().stream() - .collect(ImmutableCollectors.toMap( - e -> CompatibilityUtil.convertContexts(e.getKey()), - Map.Entry::getValue - ))).join(); - } - - @Override - public @NonNull Map getPermissions(@NonNull Set contexts) { - return handle().thenApply(handle -> handle.getPermissions(CompatibilityUtil.convertContexts(contexts))).join(); - } - - @Override - public boolean setPermission(@NonNull Set contexts, @NonNull String permission, @NonNull Tristate value) { - handle().thenCompose(handle -> handle.setPermission( - CompatibilityUtil.convertContexts(contexts), - permission, - CompatibilityUtil.convertTristate(value) - )); - return true; - } - - @Override - public boolean clearPermissions() { - handle().thenCompose(LPSubjectData::clearPermissions); - return true; - } - - @Override - public boolean clearPermissions(@NonNull Set contexts) { - handle().thenCompose(handle -> handle.clearPermissions(CompatibilityUtil.convertContexts(contexts))); - return true; - } - - @SuppressWarnings("rawtypes") - @Override - public @NonNull Map, List> getAllParents() { - return (Map) handle().thenApply(handle -> handle.getAllParents().entrySet().stream() - .collect(ImmutableCollectors.toMap( - e -> CompatibilityUtil.convertContexts(e.getKey()), - e -> e.getValue().stream() - .map(s -> new SubjectProxy(this.service, s)) - .collect(ImmutableCollectors.toList()) - ) - )).join(); - } - - @SuppressWarnings("rawtypes") - @Override - public @NonNull List getParents(@NonNull Set contexts) { - return (List) handle().thenApply(handle -> handle.getParents(CompatibilityUtil.convertContexts(contexts)).stream() - .map(s -> new SubjectProxy(this.service, s)) - .collect(ImmutableCollectors.toList())).join(); - } - - @Override - public boolean addParent(@NonNull Set contexts, @NonNull Subject parent) { - handle().thenCompose(handle -> handle.addParent( - CompatibilityUtil.convertContexts(contexts), - this.service.getReferenceFactory().obtain(parent) - )); - return true; - } - - @Override - public boolean removeParent(@NonNull Set contexts, @NonNull Subject parent) { - handle().thenCompose(handle -> handle.removeParent( - CompatibilityUtil.convertContexts(contexts), - this.service.getReferenceFactory().obtain(parent) - )); - return true; - } - - @Override - public boolean clearParents() { - handle().thenCompose(LPSubjectData::clearParents); - return true; - } - - @Override - public boolean clearParents(@NonNull Set contexts) { - handle().thenCompose(handle -> handle.clearParents(CompatibilityUtil.convertContexts(contexts))); - return true; - } - - @SuppressWarnings("rawtypes") - @Override - public @NonNull Map, Map> getAllOptions() { - return (Map) handle().thenApply(handle -> handle.getAllOptions().entrySet().stream() - .collect(ImmutableCollectors.toMap( - e -> CompatibilityUtil.convertContexts(e.getKey()), - Map.Entry::getValue - ))).join(); - } - - @Override - public @NonNull Map getOptions(@NonNull Set contexts) { - return handle().thenApply(handle -> handle.getOptions(CompatibilityUtil.convertContexts(contexts))).join(); - } - - @Override - public boolean setOption(@NonNull Set contexts, @NonNull String key, String value) { - if (value == null) { - handle().thenCompose(handle -> handle.unsetOption(CompatibilityUtil.convertContexts(contexts), key)); - } else { - handle().thenCompose(handle -> handle.setOption(CompatibilityUtil.convertContexts(contexts), key, value)); - } - return true; - } - - @Override - public boolean clearOptions(@NonNull Set contexts) { - handle().thenCompose(handle -> handle.clearOptions(CompatibilityUtil.convertContexts(contexts))); - return true; - } - - @Override - public boolean clearOptions() { - handle().thenCompose(LPSubjectData::clearOptions); - return true; - } - - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (!(o instanceof SubjectDataProxy)) return false; - final SubjectDataProxy other = (SubjectDataProxy) o; - return this.ref.equals(other.ref) && this.enduring == other.enduring; - } - - @Override - public int hashCode() { - final int PRIME = 59; - int result = 1; - result = result * PRIME + this.ref.hashCode(); - result = result * PRIME + (this.enduring ? 79 : 97); - return result; - } - - @Override - public String toString() { - return "luckperms.api6.SubjectDataProxy(ref=" + this.ref + ", enduring=" + this.enduring + ")"; - } -} diff --git a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/SubjectProxy.java b/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/SubjectProxy.java deleted file mode 100644 index 1457784cb..000000000 --- a/sponge/sponge-service-api6/src/main/java/me/lucko/luckperms/sponge/service/proxy/api6/SubjectProxy.java +++ /dev/null @@ -1,187 +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.sponge.service.proxy.api6; - -import me.lucko.luckperms.common.context.QueryOptionsSupplier; -import me.lucko.luckperms.common.util.ImmutableCollectors; -import me.lucko.luckperms.sponge.service.CompatibilityUtil; -import me.lucko.luckperms.sponge.service.model.LPPermissionService; -import me.lucko.luckperms.sponge.service.model.LPSubject; -import me.lucko.luckperms.sponge.service.model.LPSubjectReference; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; -import me.lucko.luckperms.sponge.service.model.ProxiedSubject; - -import net.luckperms.api.query.QueryOptions; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.command.CommandSource; -import org.spongepowered.api.service.context.Context; -import org.spongepowered.api.service.permission.Subject; -import org.spongepowered.api.service.permission.SubjectCollection; -import org.spongepowered.api.service.permission.SubjectData; -import org.spongepowered.api.util.Tristate; - -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; - -@SuppressWarnings("unchecked") -public final class SubjectProxy implements Subject, ProxiedSubject, ProxiedServiceObject { - private final LPPermissionService service; - private final LPSubjectReference ref; - - private QueryOptionsSupplier queryOptionsSupplier = null; - - public SubjectProxy(LPPermissionService service, LPSubjectReference ref) { - this.service = service; - this.ref = ref; - } - - private CompletableFuture handle() { - return this.ref.resolveLp(); - } - - // lazy init - private QueryOptionsSupplier getContextsCache() { - if (this.queryOptionsSupplier == null) { - this.queryOptionsSupplier = this.service.getContextManager().getCacheFor(this); - } - return this.queryOptionsSupplier; - } - - @Override - public @NonNull LPSubjectReference asSubjectReference() { - return this.ref; - } - - @Override - public @NonNull QueryOptions getQueryOptions() { - return getContextsCache().getQueryOptions(); - } - - @Override - public @NonNull Optional getCommandSource() { - return handle().thenApply(LPSubject::getCommandSource).join(); - } - - @Override - public @NonNull SubjectCollection getContainingCollection() { - return this.service.getCollection(this.ref.getCollectionIdentifier()).sponge(); - } - - @Override - public SubjectData getSubjectData() { - return new SubjectDataProxy(this.service, this.ref, true); - } - - @Override - public SubjectData getTransientSubjectData() { - return new SubjectDataProxy(this.service, this.ref, false); - } - - @Override - public boolean hasPermission(@NonNull Set contexts, @NonNull String permission) { - return handle().thenApply(handle -> handle.getPermissionValue(CompatibilityUtil.convertContexts(contexts), permission).asBoolean()).join(); - } - - @Override - public boolean hasPermission(@NonNull String permission) { - return handle().thenApply(handle -> handle.getPermissionValue(getContextsCache().getContextSet(), permission).asBoolean()).join(); - } - - @Override - public @NonNull Tristate getPermissionValue(@NonNull Set contexts, @NonNull String permission) { - return handle().thenApply(handle -> CompatibilityUtil.convertTristate(handle.getPermissionValue(CompatibilityUtil.convertContexts(contexts), permission))).join(); - } - - @Override - public boolean isChildOf(@NonNull Subject parent) { - return handle().thenApply(handle -> handle.isChildOf( - getContextsCache().getContextSet(), - this.service.getReferenceFactory().obtain(parent) - )).join(); - } - - @Override - public boolean isChildOf(@NonNull Set contexts, @NonNull Subject parent) { - return handle().thenApply(handle -> handle.isChildOf( - CompatibilityUtil.convertContexts(contexts), - this.service.getReferenceFactory().obtain(parent) - )).join(); - } - - @SuppressWarnings("rawtypes") - @Override - public @NonNull List getParents() { - return (List) handle().thenApply(handle -> handle.getParents(getContextsCache().getContextSet()).stream() - .map(s -> new SubjectProxy(this.service, s)) - .collect(ImmutableCollectors.toList())).join(); - } - - @SuppressWarnings("rawtypes") - @Override - public @NonNull List getParents(@NonNull Set contexts) { - return (List) handle().thenApply(handle -> handle.getParents(CompatibilityUtil.convertContexts(contexts)).stream() - .map(s -> new SubjectProxy(this.service, s)) - .collect(ImmutableCollectors.toList())).join(); - } - - @Override - public @NonNull Optional getOption(@NonNull Set contexts, @NonNull String key) { - return handle().thenApply(handle -> handle.getOption(CompatibilityUtil.convertContexts(contexts), key)).join(); - } - - @Override - public @NonNull Optional getOption(@NonNull String key) { - return handle().thenApply(handle -> handle.getOption(getContextsCache().getContextSet(), key)).join(); - } - - @Override - public String getIdentifier() { - return this.ref.getSubjectIdentifier(); - } - - @Override - public @NonNull Set getActiveContexts() { - return CompatibilityUtil.convertContexts(getContextsCache().getContextSet()); - } - - @Override - public boolean equals(Object o) { - return o == this || o instanceof SubjectProxy && this.ref.equals(((SubjectProxy) o).ref); - } - - @Override - public int hashCode() { - return this.ref.hashCode(); - } - - @Override - public String toString() { - return "luckperms.api6.SubjectProxy(ref=" + this.ref + ")"; - } -} diff --git a/sponge/sponge-service-api7/build.gradle b/sponge/sponge-service-api7/build.gradle deleted file mode 100644 index dadbbb378..000000000 --- a/sponge/sponge-service-api7/build.gradle +++ /dev/null @@ -1,15 +0,0 @@ -repositories { - maven { url 'https://repo.spongepowered.org/maven' } -} - -dependencies { - compile project(':common') - compile project(':sponge:sponge-service') - - compileOnly('org.spongepowered:spongeapi:7.3.0') { - exclude(module: 'configurate-core') - exclude(module: 'configurate-hocon') - exclude(module: 'configurate-gson') - exclude(module: 'configurate-yaml') - } -} diff --git a/sponge/sponge-service-proxy/build.gradle b/sponge/sponge-service-proxy/build.gradle new file mode 100644 index 000000000..b9a27a5a1 --- /dev/null +++ b/sponge/sponge-service-proxy/build.gradle @@ -0,0 +1,19 @@ +repositories { + maven { url 'https://repo.spongepowered.org/repository/maven-public/' } +} + +tasks.withType(JavaCompile).configureEach { + options.release = 21 +} + +dependencies { + implementation project(':common') + implementation project(':sponge:sponge-service') + + compileOnly('org.spongepowered:spongeapi:12.0.0') { + exclude(module: 'configurate-core') + exclude(module: 'configurate-hocon') + exclude(module: 'configurate-gson') + exclude(module: 'configurate-yaml') + } +} diff --git a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/DescriptionBuilder.java b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/DescriptionBuilder.java similarity index 80% rename from sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/DescriptionBuilder.java rename to sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/DescriptionBuilder.java index 40b50a730..ce4b06c28 100644 --- a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/DescriptionBuilder.java +++ b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/DescriptionBuilder.java @@ -23,42 +23,32 @@ * SOFTWARE. */ -package me.lucko.luckperms.sponge.service.proxy.api7; +package me.lucko.luckperms.sponge.service.proxy; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.sponge.service.model.LPPermissionDescription; import me.lucko.luckperms.sponge.service.model.LPPermissionService; +import me.lucko.luckperms.sponge.service.model.LPProxiedServiceObject; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - +import net.kyori.adventure.text.Component; import net.luckperms.api.util.Tristate; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.spongepowered.api.plugin.PluginContainer; import org.spongepowered.api.service.permission.PermissionDescription; import org.spongepowered.api.service.permission.PermissionService; -import org.spongepowered.api.text.Text; +import org.spongepowered.plugin.PluginContainer; import java.util.HashMap; import java.util.Map; import java.util.Objects; -public final class DescriptionBuilder implements PermissionDescription.Builder, ProxiedServiceObject { - - public static LPPermissionDescription registerDescription(LPPermissionService service, PermissionDescription description) { - if (!description.getOwner().isPresent()) { - return null; - } - return service.registerPermissionDescription(description.getId(), description.getDescription().orElse(null), description.getOwner().get()); - } - +public final class DescriptionBuilder implements PermissionDescription.Builder, LPProxiedServiceObject { private final @NonNull LPPermissionService service; private final @NonNull PluginContainer container; private final @NonNull Map roles = new HashMap<>(); private @Nullable String id = null; - private @Nullable Text description = null; + private @Nullable Component description = null; public DescriptionBuilder(LPPermissionService service, PluginContainer container) { this.service = Objects.requireNonNull(service, "service"); @@ -72,7 +62,7 @@ public DescriptionBuilder(LPPermissionService service, PluginContainer container } @Override - public PermissionDescription.@NonNull Builder description(@Nullable Text description) { + public PermissionDescription.@NonNull Builder description(@Nullable Component description) { this.description = description; return this; } @@ -84,6 +74,11 @@ public DescriptionBuilder(LPPermissionService service, PluginContainer container return this; } + @Override + public PermissionDescription.Builder defaultValue(org.spongepowered.api.util.Tristate defaultValue) { + throw new UnsupportedOperationException("LuckPerms does not support assigning a default value to permission descriptions"); + } + @Override public @NonNull PermissionDescription register() throws IllegalStateException { if (this.id == null) { @@ -121,13 +116,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - final int PRIME = 59; - int result = 1; - result = result * PRIME + this.container.hashCode(); - result = result * PRIME + this.roles.hashCode(); - result = result * PRIME + (this.id == null ? 43 : this.id.hashCode()); - result = result * PRIME + (this.description == null ? 43 : this.description.hashCode()); - return result; + return Objects.hash(this.container, this.roles, this.id, this.description); } @Override diff --git a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/PermissionDescriptionProxy.java b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/PermissionDescriptionProxy.java similarity index 64% rename from sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/PermissionDescriptionProxy.java rename to sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/PermissionDescriptionProxy.java index e9ea3fee7..f44f12f21 100644 --- a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/PermissionDescriptionProxy.java +++ b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/PermissionDescriptionProxy.java @@ -23,25 +23,27 @@ * SOFTWARE. */ -package me.lucko.luckperms.sponge.service.proxy.api7; +package me.lucko.luckperms.sponge.service.proxy; import me.lucko.luckperms.common.util.ImmutableCollectors; import me.lucko.luckperms.sponge.service.model.LPPermissionDescription; import me.lucko.luckperms.sponge.service.model.LPPermissionService; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - +import me.lucko.luckperms.sponge.service.model.LPProxiedServiceObject; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.plugin.PluginContainer; +import org.spongepowered.api.ResourceKey; import org.spongepowered.api.service.permission.PermissionDescription; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectReference; -import org.spongepowered.api.text.Text; +import org.spongepowered.api.util.Tristate; +import org.spongepowered.plugin.PluginContainer; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; -public final class PermissionDescriptionProxy implements PermissionDescription, ProxiedServiceObject { +public final class PermissionDescriptionProxy implements PermissionDescription, LPProxiedServiceObject { private final LPPermissionService service; private final LPPermissionDescription handle; @@ -51,22 +53,27 @@ public PermissionDescriptionProxy(LPPermissionService service, LPPermissionDescr } @Override - public @NonNull String getId() { + public @NonNull String id() { return this.handle.getId(); } @Override - public @NonNull Optional getDescription() { + public @NonNull Optional description() { return this.handle.getDescription(); } @Override - public @NonNull Optional getOwner() { + public @NonNull Optional owner() { return this.handle.getOwner(); } @Override - public @NonNull Map getAssignedSubjects(@NonNull String s) { + public Tristate defaultValue() { + return Tristate.UNDEFINED; + } + + @Override + public @NonNull Map assignedSubjects(@NonNull String s) { return this.handle.getAssignedSubjects(s).entrySet().stream() .collect(ImmutableCollectors.toMap( e -> new SubjectProxy(this.service, e.getKey().toReference()), @@ -74,6 +81,37 @@ public PermissionDescriptionProxy(LPPermissionService service, LPPermissionDescr )); } + @Override + public boolean query(Subject subj) { + return subj.hasPermission(this.handle.getId()); + } + + @Override + public boolean query(Subject subj, String parameter) { + Objects.requireNonNull(parameter, "parameter"); + return subj.hasPermission(this.handle.getId() + '.' + parameter); + } + + @Override + public boolean query(Subject subj, ResourceKey key) { + return query(subj, key.namespace() + '.' + key.value()); + } + + @Override + public boolean query(Subject subj, String... parameters) { + if (parameters.length == 0) { + return this.query(subj); + } else if (parameters.length == 1) { + return this.query(subj, parameters[0]); + } + + StringBuilder builder = new StringBuilder(this.handle.getId()); + for (String parameter : parameters) { + builder.append('.').append(parameter); + } + return subj.hasPermission(builder.toString()); + } + @Override @SuppressWarnings({"unchecked", "rawtypes"}) public @NonNull CompletableFuture> findAssignedSubjects(@NonNull String s) { @@ -92,6 +130,6 @@ public int hashCode() { @Override public String toString() { - return "luckperms.api7.PermissionDescriptionProxy(handle=" + this.handle + ")"; + return "luckperms.PermissionDescriptionProxy(handle=" + this.handle + ")"; } } diff --git a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/PermissionServiceProxy.java b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/PermissionServiceProxy.java similarity index 70% rename from sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/PermissionServiceProxy.java rename to sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/PermissionServiceProxy.java index 91b61909a..681bd8a98 100644 --- a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/PermissionServiceProxy.java +++ b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/PermissionServiceProxy.java @@ -23,27 +23,29 @@ * SOFTWARE. */ -package me.lucko.luckperms.sponge.service.proxy.api7; +package me.lucko.luckperms.sponge.service.proxy; import com.google.common.collect.ImmutableSet; - import me.lucko.luckperms.common.util.ImmutableCollectors; +import me.lucko.luckperms.sponge.service.CompatibilityUtil; +import me.lucko.luckperms.sponge.service.PermissionAndContextService; import me.lucko.luckperms.sponge.service.model.LPPermissionDescription; import me.lucko.luckperms.sponge.service.model.LPPermissionService; +import me.lucko.luckperms.sponge.service.model.LPProxiedServiceObject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.Sponge; -import org.spongepowered.api.plugin.PluginContainer; +import org.spongepowered.api.event.Cause; +import org.spongepowered.api.service.context.Context; import org.spongepowered.api.service.context.ContextCalculator; import org.spongepowered.api.service.permission.PermissionDescription; import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectCollection; import org.spongepowered.api.service.permission.SubjectReference; +import org.spongepowered.plugin.PluginContainer; import java.util.Collection; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -51,7 +53,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; -public final class PermissionServiceProxy implements PermissionService, ProxiedServiceObject { +public final class PermissionServiceProxy implements PermissionAndContextService, LPProxiedServiceObject { private final LPPermissionService handle; public PermissionServiceProxy(LPPermissionService handle) { @@ -59,22 +61,22 @@ public PermissionServiceProxy(LPPermissionService handle) { } @Override - public @NonNull SubjectCollection getUserSubjects() { + public @NonNull SubjectCollection userSubjects() { return this.handle.getUserSubjects().sponge(); } @Override - public @NonNull SubjectCollection getGroupSubjects() { + public @NonNull SubjectCollection groupSubjects() { return this.handle.getGroupSubjects().sponge(); } @Override - public @NonNull Subject getDefaults() { + public @NonNull Subject defaults() { return this.handle.getRootDefaults().sponge(); } @Override - public @NonNull Predicate getIdentifierValidityPredicate() { + public @NonNull Predicate identifierValidityPredicate() { return this.handle.getIdentifierValidityPredicate(); } @@ -84,17 +86,17 @@ public CompletableFuture loadCollection(@NonNull String s) { } @Override - public @NonNull Optional getCollection(String s) { - return Optional.ofNullable(this.handle.getLoadedCollections().get(s.toLowerCase())).map(LPSubjectCollection::sponge); + public @NonNull Optional collection(String s) { + return Optional.ofNullable(this.handle.getLoadedCollections().get(s.toLowerCase(Locale.ROOT))).map(LPSubjectCollection::sponge); } @Override public CompletableFuture hasCollection(String s) { - return CompletableFuture.completedFuture(this.handle.getLoadedCollections().containsKey(s.toLowerCase())); + return CompletableFuture.completedFuture(this.handle.getLoadedCollections().containsKey(s.toLowerCase(Locale.ROOT))); } @Override - public @NonNull Map getLoadedCollections() { + public @NonNull Map loadedCollections() { return this.handle.getLoadedCollections().entrySet().stream() .collect(ImmutableCollectors.toMap( Map.Entry::getKey, @@ -103,7 +105,7 @@ public CompletableFuture hasCollection(String s) { } @Override - public CompletableFuture> getAllIdentifiers() { + public CompletableFuture> allIdentifiers() { return CompletableFuture.completedFuture(ImmutableSet.copyOf(this.handle.getLoadedCollections().keySet())); } @@ -113,10 +115,11 @@ public CompletableFuture> getAllIdentifiers() { Objects.requireNonNull(subjectIdentifier, "subjectIdentifier"); // test the identifiers - String collection = collectionIdentifier.toLowerCase(); - if (collection.equals("user") && !this.handle.getUserSubjects().getIdentifierValidityPredicate().test(subjectIdentifier)) { + if (collectionIdentifier.equalsIgnoreCase(PermissionService.SUBJECTS_USER) && + !this.handle.getUserSubjects().getIdentifierValidityPredicate().test(subjectIdentifier)) { throw new IllegalArgumentException("Subject identifier '" + subjectIdentifier + "' does not pass the validity predicate for the user subject collection"); - } else if (collection.equals("group") && !this.handle.getGroupSubjects().getIdentifierValidityPredicate().test(subjectIdentifier)) { + } else if (collectionIdentifier.equalsIgnoreCase(PermissionService.SUBJECTS_GROUP) && + !this.handle.getGroupSubjects().getIdentifierValidityPredicate().test(subjectIdentifier)) { throw new IllegalArgumentException("Subject identifier '" + subjectIdentifier + "' does not pass the validity predicate for the group subject collection"); } @@ -125,27 +128,32 @@ public CompletableFuture> getAllIdentifiers() { } @Override - public PermissionDescription.Builder newDescriptionBuilder(@NonNull Object o) { - Optional container = Sponge.getGame().getPluginManager().fromInstance(o); - if (!container.isPresent()) { - throw new IllegalArgumentException("Couldn't find a plugin container for " + o.getClass().getSimpleName()); - } - - return new DescriptionBuilder(this.handle, container.get()); + public PermissionDescription.Builder newDescriptionBuilder(@NonNull PluginContainer container) { + return new DescriptionBuilder(this.handle, container); } @Override - public @NonNull Optional getDescription(@NonNull String s) { + public @NonNull Optional description(@NonNull String s) { return this.handle.getDescription(s).map(LPPermissionDescription::sponge); } @Override - public @NonNull Collection getDescriptions() { + public @NonNull Collection descriptions() { return this.handle.getDescriptions().stream().map(LPPermissionDescription::sponge).collect(ImmutableCollectors.toSet()); } @Override - public void registerContextCalculator(@NonNull ContextCalculator contextCalculator) { + public Set contexts() { + return CompatibilityUtil.convertContexts(this.handle.getContextsForCurrentCause()); + } + + @Override + public Set contextsFor(Cause cause) { + return CompatibilityUtil.convertContexts(this.handle.getContextsForCause(cause)); + } + + @Override + public void registerContextCalculator(@NonNull ContextCalculator contextCalculator) { this.handle.registerContextCalculator(contextCalculator); } @@ -161,6 +169,6 @@ public int hashCode() { @Override public String toString() { - return "luckperms.api7.PermissionServiceProxy(handle=" + this.handle + ")"; + return "luckperms.PermissionServiceProxy(handle=" + this.handle + ")"; } } diff --git a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/SubjectCollectionProxy.java b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/SubjectCollectionProxy.java similarity index 75% rename from sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/SubjectCollectionProxy.java rename to sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/SubjectCollectionProxy.java index 660be87ea..50fde0636 100644 --- a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/SubjectCollectionProxy.java +++ b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/SubjectCollectionProxy.java @@ -23,16 +23,14 @@ * SOFTWARE. */ -package me.lucko.luckperms.sponge.service.proxy.api7; +package me.lucko.luckperms.sponge.service.proxy; import me.lucko.luckperms.common.util.ImmutableCollectors; -import me.lucko.luckperms.sponge.service.CompatibilityUtil; +import me.lucko.luckperms.sponge.service.model.LPProxiedServiceObject; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.service.context.Context; +import org.spongepowered.api.event.Cause; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectCollection; import org.spongepowered.api.service.permission.SubjectReference; @@ -45,8 +43,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; -@SuppressWarnings("unchecked") -public final class SubjectCollectionProxy implements SubjectCollection, ProxiedServiceObject { +public final class SubjectCollectionProxy implements SubjectCollection, LPProxiedServiceObject { private final LPSubjectCollection handle; public SubjectCollectionProxy(LPSubjectCollection handle) { @@ -54,12 +51,12 @@ public SubjectCollectionProxy(LPSubjectCollection handle) { } @Override - public @NonNull String getIdentifier() { + public @NonNull String identifier() { return this.handle.getIdentifier(); } @Override - public @NonNull Predicate getIdentifierValidityPredicate() { + public @NonNull Predicate identifierValidityPredicate() { return this.handle.getIdentifierValidityPredicate(); } @@ -69,7 +66,7 @@ public SubjectCollectionProxy(LPSubjectCollection handle) { } @Override - public @NonNull Optional getSubject(@NonNull String s) { + public @NonNull Optional subject(@NonNull String s) { return this.handle.getSubject(s).map(LPSubject::sponge); } @@ -79,18 +76,18 @@ public SubjectCollectionProxy(LPSubjectCollection handle) { } @Override - public @NonNull CompletableFuture> loadSubjects(@NonNull Set set) { - return this.handle.loadSubjects(set).thenApply(subs -> subs.stream().collect(ImmutableCollectors.toMap(LPSubject::getIdentifier, LPSubject::sponge))); + public @NonNull CompletableFuture> loadSubjects(@NonNull Iterable set) { + return this.handle.loadSubjects(set).thenApply(subs -> subs.stream().collect(ImmutableCollectors.toMap(lpSubject -> lpSubject.getIdentifier().getName(), LPSubject::sponge))); } @Override - public @NonNull Collection getLoadedSubjects() { + public @NonNull Collection loadedSubjects() { return this.handle.getLoadedSubjects().stream().map(LPSubject::sponge).collect(ImmutableCollectors.toSet()); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public @NonNull CompletableFuture> getAllIdentifiers() { + public @NonNull CompletableFuture> allIdentifiers() { return (CompletableFuture) this.handle.getAllIdentifiers(); } @@ -101,23 +98,23 @@ public SubjectCollectionProxy(LPSubjectCollection handle) { throw new IllegalArgumentException("Subject identifier '" + subjectIdentifier + "' does not pass the validity predicate"); } - return this.handle.getService().getReferenceFactory().obtain(getIdentifier(), subjectIdentifier); + return this.handle.getService().getReferenceFactory().obtain(identifier(), subjectIdentifier); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public @NonNull CompletableFuture> getAllWithPermission(@NonNull String s) { + public @NonNull CompletableFuture> allWithPermission(@NonNull String s) { return (CompletableFuture) this.handle.getAllWithPermission(s); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public @NonNull CompletableFuture> getAllWithPermission(@NonNull Set set, @NonNull String s) { - return (CompletableFuture) this.handle.getAllWithPermission(CompatibilityUtil.convertContexts(set), s); + public @NonNull CompletableFuture> allWithPermission(@NonNull String s, @NonNull Cause cause) { + return (CompletableFuture) this.handle.getAllWithPermission(this.handle.getService().getContextsForCause(cause), s); } @Override - public @NonNull Map getLoadedWithPermission(@NonNull String s) { + public @NonNull Map loadedWithPermission(@NonNull String s) { return this.handle.getLoadedWithPermission(s).entrySet().stream() .collect(ImmutableCollectors.toMap( sub -> sub.getKey().sponge(), @@ -126,8 +123,8 @@ public SubjectCollectionProxy(LPSubjectCollection handle) { } @Override - public @NonNull Map getLoadedWithPermission(@NonNull Set set, @NonNull String s) { - return this.handle.getLoadedWithPermission(CompatibilityUtil.convertContexts(set), s).entrySet().stream() + public @NonNull Map loadedWithPermission(@NonNull String s, @NonNull Cause cause) { + return this.handle.getLoadedWithPermission(this.handle.getService().getContextsForCause(cause), s).entrySet().stream() .collect(ImmutableCollectors.toMap( sub -> sub.getKey().sponge(), Map.Entry::getValue @@ -135,7 +132,7 @@ public SubjectCollectionProxy(LPSubjectCollection handle) { } @Override - public @NonNull Subject getDefaults() { + public @NonNull Subject defaults() { return this.handle.getDefaults().sponge(); } @@ -156,7 +153,7 @@ public int hashCode() { @Override public String toString() { - return "luckperms.api7.SubjectCollectionProxy(handle=" + this.handle + ")"; + return "luckperms.SubjectCollectionProxy(handle=" + this.handle + ")"; } } diff --git a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/SubjectDataProxy.java b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/SubjectDataProxy.java similarity index 66% rename from sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/SubjectDataProxy.java rename to sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/SubjectDataProxy.java index 588454ab9..e55381b35 100644 --- a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/SubjectDataProxy.java +++ b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/SubjectDataProxy.java @@ -23,29 +23,33 @@ * SOFTWARE. */ -package me.lucko.luckperms.sponge.service.proxy.api7; +package me.lucko.luckperms.sponge.service.proxy; +import com.google.common.collect.ImmutableMap; +import me.lucko.luckperms.common.util.CompletableFutures; import me.lucko.luckperms.common.util.ImmutableCollectors; import me.lucko.luckperms.sponge.service.CompatibilityUtil; import me.lucko.luckperms.sponge.service.model.LPPermissionService; +import me.lucko.luckperms.sponge.service.model.LPProxiedServiceObject; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectData; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; - import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.api.service.context.Context; +import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectData; +import org.spongepowered.api.service.permission.SubjectReference; +import org.spongepowered.api.service.permission.TransferMethod; import org.spongepowered.api.util.Tristate; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; -@SuppressWarnings("unchecked") -public final class SubjectDataProxy implements SubjectData, ProxiedServiceObject { +public final class SubjectDataProxy implements SubjectData, LPProxiedServiceObject { private final LPPermissionService service; private final LPSubjectReference ref; private final boolean enduring; @@ -62,9 +66,19 @@ private CompletableFuture handle() { this.ref.resolveLp().thenApply(LPSubject::getTransientSubjectData); } - @SuppressWarnings("rawtypes") @Override - public @NonNull Map, Map> getAllPermissions() { + public Subject subject() { + return handle().thenApply(handle -> handle.getParentSubject().sponge()).join(); + } + + @Override + public boolean isTransient() { + return !this.enduring; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public @NonNull Map, Map> allPermissions() { return (Map) handle().thenApply(handle -> handle.getAllPermissions().entrySet().stream() .collect(ImmutableCollectors.toMap( e -> CompatibilityUtil.convertContexts(e.getKey()), @@ -73,7 +87,7 @@ private CompletableFuture handle() { } @Override - public @NonNull Map getPermissions(@NonNull Set contexts) { + public @NonNull Map permissions(@NonNull Set contexts) { return handle().thenApply(handle -> handle.getPermissions(CompatibilityUtil.convertContexts(contexts))).join(); } @@ -86,6 +100,43 @@ private CompletableFuture handle() { )); } + @Override + public CompletableFuture setPermissions(Set contexts, Map permissions, TransferMethod method) { + CompletableFuture fut; + if (method == TransferMethod.OVERWRITE) { + fut = clearPermissions(contexts); + } else { + fut = CompletableFuture.completedFuture(true); + } + + return fut.thenCompose(bool -> permissions.entrySet().stream() + .map(entry -> setPermission(contexts, entry.getKey(), Tristate.fromBoolean(entry.getValue()))) + .collect(CompletableFutures.collector()) + ) + .thenApply(x -> true); + } + + @Override + public Tristate fallbackPermissionValue(Set contexts) { + // TODO: check subject type here to return a more appropriate value? + return Tristate.UNDEFINED; + } + + @Override + public Map, Tristate> allFallbackPermissionValues() { + return ImmutableMap.of(); + } + + @Override + public CompletableFuture setFallbackPermissionValue(Set contexts, Tristate fallback) { + throw new UnsupportedOperationException("LuckPerms does not support setting fallback permission values"); + } + + @Override + public CompletableFuture clearFallbackPermissionValues() { + return CompletableFuture.completedFuture(true); + } + @Override public @NonNull CompletableFuture clearPermissions() { return handle().thenCompose(LPSubjectData::clearPermissions); @@ -96,9 +147,9 @@ private CompletableFuture handle() { return handle().thenCompose(handle -> handle.clearPermissions(CompatibilityUtil.convertContexts(contexts))); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public @NonNull Map, List> getAllParents() { + public @NonNull Map, List> allParents() { return (Map) handle().thenApply(handle -> handle.getAllParents().entrySet().stream() .collect(ImmutableCollectors.toMap( e -> CompatibilityUtil.convertContexts(e.getKey()), @@ -106,12 +157,17 @@ private CompletableFuture handle() { ))).join(); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public @NonNull List getParents(@NonNull Set contexts) { + public @NonNull List parents(@NonNull Set contexts) { return (List) handle().thenApply(handle -> handle.getParents(CompatibilityUtil.convertContexts(contexts))).join(); } + @Override + public CompletableFuture setParents(Set contexts, List parents, TransferMethod method) { + return null; + } + @Override public @NonNull CompletableFuture addParent(@NonNull Set contexts, org.spongepowered.api.service.permission.@NonNull SubjectReference ref) { return handle().thenCompose(handle -> handle.addParent(CompatibilityUtil.convertContexts(contexts), this.service.getReferenceFactory().obtain(ref))); @@ -132,9 +188,9 @@ private CompletableFuture handle() { return handle().thenCompose(handle -> handle.clearParents(CompatibilityUtil.convertContexts(contexts))); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public @NonNull Map, Map> getAllOptions() { + public @NonNull Map, Map> allOptions() { return (Map) handle().thenApply(handle -> handle.getAllOptions().entrySet().stream() .collect(ImmutableCollectors.toMap( e -> CompatibilityUtil.convertContexts(e.getKey()), @@ -143,7 +199,7 @@ private CompletableFuture handle() { } @Override - public @NonNull Map getOptions(@NonNull Set contexts) { + public @NonNull Map options(@NonNull Set contexts) { return handle().thenApply(handle -> handle.getOptions(CompatibilityUtil.convertContexts(contexts))).join(); } @@ -156,6 +212,11 @@ private CompletableFuture handle() { } } + @Override + public CompletableFuture setOptions(Set contexts, Map options, TransferMethod method) { + return null; + } + @Override public @NonNull CompletableFuture clearOptions() { return handle().thenCompose(LPSubjectData::clearOptions); @@ -166,6 +227,16 @@ private CompletableFuture handle() { return handle().thenCompose(handle -> handle.clearOptions(CompatibilityUtil.convertContexts(contexts))); } + @Override + public CompletableFuture copyFrom(SubjectData other, TransferMethod method) { + return null; + } + + @Override + public CompletableFuture moveFrom(SubjectData other, TransferMethod method) { + return null; + } + @Override public boolean equals(Object o) { if (o == this) return true; @@ -176,15 +247,11 @@ public boolean equals(Object o) { @Override public int hashCode() { - final int PRIME = 59; - int result = 1; - result = result * PRIME + this.ref.hashCode(); - result = result * PRIME + (this.enduring ? 79 : 97); - return result; + return Objects.hash(this.ref, this.enduring); } @Override public String toString() { - return "luckperms.api7.SubjectDataProxy(ref=" + this.ref + ", enduring=" + this.enduring + ")"; + return "luckperms.SubjectDataProxy(ref=" + this.ref + ", enduring=" + this.enduring + ")"; } } diff --git a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/SubjectProxy.java b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/SubjectProxy.java similarity index 55% rename from sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/SubjectProxy.java rename to sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/SubjectProxy.java index 91d30e85d..c0497dabe 100644 --- a/sponge/sponge-service-api7/src/main/java/me/lucko/luckperms/sponge/service/proxy/api7/SubjectProxy.java +++ b/sponge/sponge-service-proxy/src/main/java/me/lucko/luckperms/sponge/service/proxy/SubjectProxy.java @@ -23,21 +23,21 @@ * SOFTWARE. */ -package me.lucko.luckperms.sponge.service.proxy.api7; +package me.lucko.luckperms.sponge.service.proxy; -import me.lucko.luckperms.common.context.QueryOptionsSupplier; import me.lucko.luckperms.sponge.service.CompatibilityUtil; import me.lucko.luckperms.sponge.service.model.LPPermissionService; +import me.lucko.luckperms.sponge.service.model.LPProxiedServiceObject; +import me.lucko.luckperms.sponge.service.model.LPProxiedSubject; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; -import me.lucko.luckperms.sponge.service.model.ProxiedSubject; - +import me.lucko.luckperms.sponge.service.model.LPSubjectUser; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.command.CommandSource; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.event.Cause; import org.spongepowered.api.service.context.Context; +import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectCollection; import org.spongepowered.api.service.permission.SubjectData; @@ -49,13 +49,10 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; -@SuppressWarnings("unchecked") -public final class SubjectProxy implements Subject, ProxiedSubject, ProxiedServiceObject { +public final class SubjectProxy implements Subject, LPProxiedSubject, LPProxiedServiceObject { private final LPPermissionService service; private final LPSubjectReference ref; - private QueryOptionsSupplier queryOptionsSupplier; - public SubjectProxy(LPPermissionService service, LPSubjectReference ref) { this.service = service; this.ref = ref; @@ -65,14 +62,6 @@ private CompletableFuture handle() { return this.ref.resolveLp(); } - // lazy init - private QueryOptionsSupplier queryOptionsCache() { - if (this.queryOptionsSupplier == null) { - this.queryOptionsSupplier = this.service.getContextManager().getCacheFor(this); - } - return this.queryOptionsSupplier; - } - @Override public @NonNull LPSubjectReference asSubjectReference() { return this.ref; @@ -80,17 +69,12 @@ private QueryOptionsSupplier queryOptionsCache() { @Override public @NonNull QueryOptions getQueryOptions() { - return queryOptionsCache().getQueryOptions(); + return this.service.getContextManager().getQueryOptions(this); } @Override - public @NonNull Optional getCommandSource() { - return handle().thenApply(LPSubject::getCommandSource).join(); - } - - @Override - public @NonNull SubjectCollection getContainingCollection() { - return this.service.getCollection(this.ref.getCollectionIdentifier()).sponge(); + public @NonNull SubjectCollection containingCollection() { + return this.service.getCollection(this.ref.collectionIdentifier()).sponge(); } @Override @@ -99,75 +83,77 @@ public boolean isSubjectDataPersisted() { } @Override - public SubjectData getSubjectData() { + public SubjectData subjectData() { return new SubjectDataProxy(this.service, this.ref, true); } @Override - public SubjectData getTransientSubjectData() { + public SubjectData transientSubjectData() { return new SubjectDataProxy(this.service, this.ref, false); } @Override - public boolean hasPermission(@NonNull Set contexts, @NonNull String permission) { - return handle().thenApply(handle -> handle.getPermissionValue(CompatibilityUtil.convertContexts(contexts), permission).asBoolean()).join(); - } - - @Override - public boolean hasPermission(@NonNull String permission) { - return handle().thenApply(handle -> handle.getPermissionValue(queryOptionsCache().getContextSet(), permission).asBoolean()).join(); + public @NonNull Tristate permissionValue(@NonNull String permission, @NonNull Cause cause) { + return handle().thenApply(handle -> CompatibilityUtil.convertTristate(handle.getPermissionValue(this.service.getContextsForCause(cause), permission))).join(); } @Override - public @NonNull Tristate getPermissionValue(@NonNull Set contexts, @NonNull String permission) { + public @NonNull Tristate permissionValue(@NonNull String permission, @NonNull Set contexts) { return handle().thenApply(handle -> CompatibilityUtil.convertTristate(handle.getPermissionValue(CompatibilityUtil.convertContexts(contexts), permission))).join(); } @Override - public boolean isChildOf(@NonNull SubjectReference parent) { - return handle().thenApply(handle -> handle.isChildOf(queryOptionsCache().getContextSet(), this.service.getReferenceFactory().obtain(parent))).join(); + public boolean isChildOf(@NonNull SubjectReference parent, @NonNull Cause cause) { + return handle().thenApply(handle -> handle.isChildOf(this.service.getContextsForCause(cause), this.service.getReferenceFactory().obtain(parent))).join(); } @Override - public boolean isChildOf(@NonNull Set contexts, @NonNull SubjectReference parent) { + public boolean isChildOf(@NonNull SubjectReference parent, @NonNull Set contexts) { return handle().thenApply(handle -> handle.isChildOf(CompatibilityUtil.convertContexts(contexts), this.service.getReferenceFactory().obtain(parent))).join(); } - @SuppressWarnings("rawtypes") @Override - public @NonNull List getParents() { - return (List) handle().thenApply(handle -> handle.getParents(queryOptionsCache().getContextSet())).join(); + public List parents(@NonNull Cause cause) { + return handle().thenApply(handle -> handle.getParents(this.service.getContextsForCause(cause))).join(); } - @SuppressWarnings("rawtypes") @Override - public @NonNull List getParents(@NonNull Set contexts) { - return (List) handle().thenApply(handle -> handle.getParents(CompatibilityUtil.convertContexts(contexts))).join(); + public @NonNull List parents(@NonNull Set contexts) { + return handle().thenApply(handle -> handle.getParents(CompatibilityUtil.convertContexts(contexts))).join(); } @Override - public @NonNull Optional getOption(@NonNull Set contexts, @NonNull String key) { - return handle().thenApply(handle -> handle.getOption(CompatibilityUtil.convertContexts(contexts), key)).join(); + public Optional option(@NonNull String key, @NonNull Cause cause) { + return handle().thenApply(handle -> handle.getOption(this.service.getContextsForCause(cause), key)).join(); } @Override - public @NonNull Optional getOption(@NonNull String key) { - return handle().thenApply(handle -> handle.getOption(queryOptionsCache().getContextSet(), key)).join(); + public @NonNull Optional option(@NonNull String key, @NonNull Set contexts) { + return handle().thenApply(handle -> handle.getOption(CompatibilityUtil.convertContexts(contexts), key)).join(); } @Override - public String getIdentifier() { - return this.ref.getSubjectIdentifier(); + public String identifier() { + return this.ref.subjectIdentifier(); } @Override - public @NonNull Optional getFriendlyIdentifier() { + public @NonNull Optional friendlyIdentifier() { return handle().thenApply(LPSubject::getFriendlyIdentifier).join(); } @Override - public @NonNull Set getActiveContexts() { - return CompatibilityUtil.convertContexts(queryOptionsCache().getContextSet()); + public Optional associatedObject() { + if (this.ref.collectionIdentifier().equals(PermissionService.SUBJECTS_USER)) { + LPSubject lpSubject = handle().join(); + if (lpSubject instanceof LPSubjectUser) { + ServerPlayer player = ((LPSubjectUser) lpSubject).resolvePlayer().orElse(null); + if (player != null) { + return Optional.of(player); + } + } + } + return Optional.empty(); } @Override @@ -182,6 +168,6 @@ public int hashCode() { @Override public String toString() { - return "luckperms.api7.SubjectProxy(ref=" + this.ref + ")"; + return "luckperms.SubjectProxy(ref=" + this.ref + ")"; } } diff --git a/sponge/sponge-service/build.gradle b/sponge/sponge-service/build.gradle index 6a31118e2..dd585205e 100644 --- a/sponge/sponge-service/build.gradle +++ b/sponge/sponge-service/build.gradle @@ -1,16 +1,19 @@ repositories { - maven { url 'https://repo.spongepowered.org/maven' } + maven { url 'https://repo.spongepowered.org/repository/maven-public/' } +} + +tasks.withType(JavaCompile).configureEach { + options.release = 21 } dependencies { - compile project(':common') + implementation project(':common') - compileOnly('org.spongepowered:spongeapi:7.2.0') { + compileOnly('org.spongepowered:spongeapi:12.0.0') { exclude(module: 'configurate-core') exclude(module: 'configurate-hocon') exclude(module: 'configurate-gson') exclude(module: 'configurate-yaml') } - compileOnly 'com.google.guava:guava:21.0' } diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/CompatibilityUtil.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/CompatibilityUtil.java index 8eabb3b26..11dd09c7e 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/CompatibilityUtil.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/CompatibilityUtil.java @@ -26,16 +26,13 @@ package me.lucko.luckperms.sponge.service; import com.google.common.collect.ImmutableSet; - -import me.lucko.luckperms.common.context.contextset.ContextImpl; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ContextImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.sponge.service.context.ForwardingContextSet; import me.lucko.luckperms.sponge.service.context.ForwardingImmutableContextSet; - import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.util.Tristate; - import org.spongepowered.api.service.context.Context; import java.util.Map; @@ -80,26 +77,20 @@ public static Set convertContexts(ContextSet contexts) { public static org.spongepowered.api.util.Tristate convertTristate(Tristate tristate) { Objects.requireNonNull(tristate, "tristate"); - switch (tristate) { - case TRUE: - return org.spongepowered.api.util.Tristate.TRUE; - case FALSE: - return org.spongepowered.api.util.Tristate.FALSE; - default: - return org.spongepowered.api.util.Tristate.UNDEFINED; - } + return switch (tristate) { + case TRUE -> org.spongepowered.api.util.Tristate.TRUE; + case FALSE -> org.spongepowered.api.util.Tristate.FALSE; + default -> org.spongepowered.api.util.Tristate.UNDEFINED; + }; } public static Tristate convertTristate(org.spongepowered.api.util.Tristate tristate) { Objects.requireNonNull(tristate, "tristate"); - switch (tristate) { - case TRUE: - return Tristate.TRUE; - case FALSE: - return Tristate.FALSE; - default: - return Tristate.UNDEFINED; - } + return switch (tristate) { + case TRUE -> Tristate.TRUE; + case FALSE -> Tristate.FALSE; + default -> Tristate.UNDEFINED; + }; } } diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/PermissionAndContextService.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/PermissionAndContextService.java new file mode 100644 index 000000000..d567e9bc8 --- /dev/null +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/PermissionAndContextService.java @@ -0,0 +1,33 @@ +/* + * 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.sponge.service; + +import org.spongepowered.api.service.context.ContextService; +import org.spongepowered.api.service.permission.PermissionService; + +public interface PermissionAndContextService extends PermissionService, ContextService { + +} diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/AbstractForwardingContextSet.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/AbstractForwardingContextSet.java index 69c160922..18451486e 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/AbstractForwardingContextSet.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/AbstractForwardingContextSet.java @@ -44,8 +44,7 @@ public boolean isEmpty() { @Override public boolean contains(Object o) { - if (o instanceof Context) { - Context context = (Context) o; + if (o instanceof Context context) { return !context.getKey().isEmpty() && !context.getValue().isEmpty() && delegate().contains(context.getKey(), context.getValue()); } return false; diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/ForwardingContextSet.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/ForwardingContextSet.java index a153e6443..9c7af22a1 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/ForwardingContextSet.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/ForwardingContextSet.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.sponge.service.context; import net.luckperms.api.context.ContextSet; - import org.spongepowered.api.service.context.Context; import java.util.Set; diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/ForwardingImmutableContextSet.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/ForwardingImmutableContextSet.java index c0377e10f..fa2eb332e 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/ForwardingImmutableContextSet.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/context/ForwardingImmutableContextSet.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.sponge.service.context; import net.luckperms.api.context.ImmutableContextSet; - import org.checkerframework.checker.nullness.qual.NonNull; import org.spongepowered.api.service.context.Context; diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPPermissionDescription.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPPermissionDescription.java index 72de947ad..bcbff5a3c 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPPermissionDescription.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPPermissionDescription.java @@ -25,9 +25,9 @@ package me.lucko.luckperms.sponge.service.model; -import org.spongepowered.api.plugin.PluginContainer; +import net.kyori.adventure.text.Component; import org.spongepowered.api.service.permission.PermissionDescription; -import org.spongepowered.api.text.Text; +import org.spongepowered.plugin.PluginContainer; import java.util.Map; import java.util.Optional; @@ -44,7 +44,7 @@ public interface LPPermissionDescription { String getId(); - Optional getDescription(); + Optional getDescription(); Optional getOwner(); diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPPermissionService.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPPermissionService.java index 74ea0d264..08d2d6ebb 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPPermissionService.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPPermissionService.java @@ -27,17 +27,18 @@ import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableMap; - -import me.lucko.luckperms.common.context.ContextManager; +import me.lucko.luckperms.common.context.manager.ContextManager; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.sponge.service.PermissionAndContextService; import me.lucko.luckperms.sponge.service.reference.SubjectReferenceFactory; - -import org.spongepowered.api.entity.living.player.Player; -import org.spongepowered.api.plugin.PluginContainer; +import net.kyori.adventure.text.Component; +import net.luckperms.api.context.ImmutableContextSet; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.event.Cause; import org.spongepowered.api.service.context.ContextCalculator; import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.Subject; -import org.spongepowered.api.text.Text; +import org.spongepowered.plugin.PluginContainer; import java.util.Optional; import java.util.function.Predicate; @@ -49,11 +50,11 @@ public interface LPPermissionService { LuckPermsPlugin getPlugin(); - ContextManager getContextManager(); + ContextManager getContextManager(); SubjectReferenceFactory getReferenceFactory(); - PermissionService sponge(); + PermissionAndContextService sponge(); LPSubjectCollection getUserSubjects(); @@ -69,13 +70,19 @@ public interface LPPermissionService { ImmutableMap getLoadedCollections(); - LPPermissionDescription registerPermissionDescription(String id, Text description, PluginContainer owner); + LPPermissionDescription registerPermissionDescription(String id, Component description, PluginContainer owner); Optional getDescription(String permission); ImmutableCollection getDescriptions(); - void registerContextCalculator(ContextCalculator calculator); + void registerContextCalculator(ContextCalculator calculator); + + ImmutableContextSet getContextsForCause(Cause cause); + + ImmutableContextSet getContextsForCurrentCause(); + + void fireUpdateEvent(LPSubjectData subjectData); void invalidateAllCaches(); } diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/ProxiedServiceObject.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPProxiedServiceObject.java similarity index 97% rename from sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/ProxiedServiceObject.java rename to sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPProxiedServiceObject.java index 66960fd5f..c851762c7 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/ProxiedServiceObject.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPProxiedServiceObject.java @@ -28,6 +28,6 @@ /** * Marks that an object is a proxy implementation for a PermissionService related class. */ -public interface ProxiedServiceObject { +public interface LPProxiedServiceObject { } diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/ProxiedSubject.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPProxiedSubject.java similarity index 95% rename from sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/ProxiedSubject.java rename to sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPProxiedSubject.java index edf2fe303..d5f0664fc 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/ProxiedSubject.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPProxiedSubject.java @@ -26,14 +26,13 @@ package me.lucko.luckperms.sponge.service.model; import net.luckperms.api.query.QueryOptions; - import org.checkerframework.checker.nullness.qual.NonNull; import org.spongepowered.api.service.permission.Subject; /** * Marks that an object is a proxied representation of a {@link Subject}. */ -public interface ProxiedSubject extends Subject, ProxiedServiceObject { +public interface LPProxiedSubject extends Subject, LPProxiedServiceObject { @Override @NonNull LPSubjectReference asSubjectReference(); diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubject.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubject.java index 153fb7531..90d103257 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubject.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubject.java @@ -26,12 +26,10 @@ package me.lucko.luckperms.sponge.service.model; import com.google.common.collect.ImmutableList; - +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; - -import org.spongepowered.api.command.CommandSource; import org.spongepowered.api.service.permission.Subject; import java.util.Optional; @@ -41,11 +39,11 @@ */ public interface LPSubject { - ProxiedSubject sponge(); + LPProxiedSubject sponge(); LPPermissionService getService(); - String getIdentifier(); + PermissionHolderIdentifier getIdentifier(); default LPSubjectReference toReference() { return getService().getReferenceFactory().obtain(this); @@ -57,10 +55,6 @@ default Optional getFriendlyIdentifier() { return Optional.empty(); } - default Optional getCommandSource() { - return Optional.empty(); - } - LPSubjectCollection getParentCollection(); LPSubjectData getSubjectData(); diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectCollection.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectCollection.java index cff2f416e..955dd7526 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectCollection.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectCollection.java @@ -28,14 +28,11 @@ import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.query.dataorder.DataQueryOrder; - import org.spongepowered.api.service.permission.SubjectCollection; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; @@ -71,7 +68,7 @@ default boolean isDefaultsCollection() { CompletableFuture hasRegistered(String identifier); - CompletableFuture> loadSubjects(Set identifiers); + CompletableFuture> loadSubjects(Iterable identifiers); ImmutableCollection getLoadedSubjects(); diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectData.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectData.java index 35014f268..11c869983 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectData.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectData.java @@ -27,11 +27,9 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.util.Tristate; - import org.spongepowered.api.service.permission.SubjectData; import java.util.concurrent.CompletableFuture; diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectUser.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectUser.java new file mode 100644 index 000000000..7d81230c6 --- /dev/null +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/model/LPSubjectUser.java @@ -0,0 +1,36 @@ +/* + * 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.sponge.service.model; + +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +import java.util.Optional; + +public interface LPSubjectUser extends LPSubject { + + Optional resolvePlayer(); + +} diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/reference/CachedSubjectReference.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/reference/CachedSubjectReference.java index 588389085..e00202db4 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/reference/CachedSubjectReference.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/reference/CachedSubjectReference.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.sponge.service.model.LPPermissionService; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import org.checkerframework.checker.nullness.qual.NonNull; import org.spongepowered.api.service.permission.Subject; @@ -76,12 +75,12 @@ final class CachedSubjectReference implements LPSubjectReference { } @Override - public @NonNull String getCollectionIdentifier() { + public @NonNull String collectionIdentifier() { return this.collectionIdentifier; } @Override - public @NonNull String getSubjectIdentifier() { + public @NonNull String subjectIdentifier() { return this.subjectIdentifier; } @@ -158,8 +157,8 @@ public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof LPSubjectReference)) return false; final LPSubjectReference other = (LPSubjectReference) o; - return this.collectionIdentifier.equals(other.getCollectionIdentifier()) && - this.subjectIdentifier.equals(other.getSubjectIdentifier()); + return this.collectionIdentifier.equals(other.collectionIdentifier()) && + this.subjectIdentifier.equals(other.subjectIdentifier()); } @Override diff --git a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/reference/SubjectReferenceFactory.java b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/reference/SubjectReferenceFactory.java index a57d32c29..87b8dfd42 100644 --- a/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/reference/SubjectReferenceFactory.java +++ b/sponge/sponge-service/src/main/java/me/lucko/luckperms/sponge/service/reference/SubjectReferenceFactory.java @@ -27,17 +27,16 @@ import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.base.Splitter; - import me.lucko.luckperms.common.util.CaffeineFactory; import me.lucko.luckperms.sponge.service.model.LPPermissionService; +import me.lucko.luckperms.sponge.service.model.LPProxiedSubject; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; -import me.lucko.luckperms.sponge.service.model.ProxiedSubject; - import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectReference; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -78,18 +77,18 @@ public LPSubjectReference deserialize(String serializedReference) { public LPSubjectReference obtain(LPSubject subject) { Objects.requireNonNull(subject, "subject"); - LPSubjectReference reference = obtain(subject.getParentCollection().getIdentifier(), subject.getIdentifier()); + LPSubjectReference reference = obtain(subject.getParentCollection().getIdentifier(), subject.getIdentifier().getName()); ((CachedSubjectReference) reference).fillCache(subject); return reference; } public LPSubjectReference obtain(Subject subject) { Objects.requireNonNull(subject, "subject"); - if (subject instanceof ProxiedSubject) { - return ((ProxiedSubject) subject).asSubjectReference(); + if (subject instanceof LPProxiedSubject) { + return ((LPProxiedSubject) subject).asSubjectReference(); } - return obtain(subject.getContainingCollection().getIdentifier(), subject.getIdentifier()); + return obtain(subject.containingCollection().identifier(), subject.identifier()); } public LPSubjectReference obtain(SubjectReference reference) { @@ -97,7 +96,7 @@ public LPSubjectReference obtain(SubjectReference reference) { if (reference instanceof LPSubjectReference) { return (LPSubjectReference) reference; } else { - return obtain(reference.getCollectionIdentifier(), reference.getSubjectIdentifier()); + return obtain(reference.collectionIdentifier(), reference.subjectIdentifier()); } } @@ -116,25 +115,17 @@ private static final class SubjectReferenceAttributes { private final int hashCode; private SubjectReferenceAttributes(String collectionId, String id) { - this.collectionId = collectionId.toLowerCase(); - this.id = id.toLowerCase(); - this.hashCode = calculateHashCode(); + this.collectionId = collectionId.toLowerCase(Locale.ROOT); + this.id = id.toLowerCase(Locale.ROOT); + this.hashCode = Objects.hash(this.collectionId, this.id); } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof SubjectReferenceAttributes)) return false; - final SubjectReferenceAttributes other = (SubjectReferenceAttributes) o; - return this.collectionId.equals(other.collectionId) && this.id.equals(other.id); - } - - private int calculateHashCode() { - final int PRIME = 59; - int result = 1; - result = result * PRIME + this.collectionId.hashCode(); - result = result * PRIME + this.id.hashCode(); - return result; + final SubjectReferenceAttributes that = (SubjectReferenceAttributes) o; + return this.collectionId.equals(that.collectionId) && this.id.equals(that.id); } @Override diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongeBootstrap.java b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongeBootstrap.java index fae255ed2..e7fa9be24 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongeBootstrap.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongeBootstrap.java @@ -26,36 +26,31 @@ package me.lucko.luckperms.sponge; import com.google.inject.Inject; - +import com.google.inject.Injector; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; -import me.lucko.luckperms.common.plugin.classpath.ReflectionClassPathAppender; +import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; +import me.lucko.luckperms.common.plugin.logging.Log4jPluginLogger; import me.lucko.luckperms.common.plugin.logging.PluginLogger; -import me.lucko.luckperms.common.plugin.logging.Slf4jPluginLogger; -import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; import me.lucko.luckperms.common.util.MoreFiles; - -import org.slf4j.Logger; +import net.luckperms.api.platform.Platform; +import org.apache.logging.log4j.Logger; import org.spongepowered.api.Game; -import org.spongepowered.api.Platform; +import org.spongepowered.api.Platform.Component; import org.spongepowered.api.Server; -import org.spongepowered.api.Sponge; import org.spongepowered.api.config.ConfigDir; import org.spongepowered.api.entity.living.player.Player; -import org.spongepowered.api.event.Listener; -import org.spongepowered.api.event.Order; -import org.spongepowered.api.event.game.state.GamePreInitializationEvent; -import org.spongepowered.api.event.game.state.GameStoppingServerEvent; -import org.spongepowered.api.plugin.Dependency; -import org.spongepowered.api.plugin.Plugin; -import org.spongepowered.api.plugin.PluginContainer; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.api.profile.GameProfile; -import org.spongepowered.api.scheduler.AsynchronousExecutor; -import org.spongepowered.api.scheduler.Scheduler; -import org.spongepowered.api.scheduler.SpongeExecutorService; -import org.spongepowered.api.scheduler.SynchronousExecutor; +import org.spongepowered.plugin.PluginContainer; +import org.spongepowered.plugin.metadata.PluginMetadata; import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; @@ -65,23 +60,13 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.CountDownLatch; +import java.util.function.Supplier; /** * Bootstrap plugin for LuckPerms running on Sponge. */ -@Plugin( - id = "luckperms", - name = "LuckPerms", - version = "@version@", - authors = "Luck", - description = "A permissions plugin", - url = "https://luckperms.net", - dependencies = { - // explicit dependency on spongeapi with no defined API version - @Dependency(id = "spongeapi") - } -) -public class LPSpongeBootstrap implements LuckPermsBootstrap { +public class LPSpongeBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { + private final Object loader; /** * The plugin logger @@ -91,7 +76,7 @@ public class LPSpongeBootstrap implements LuckPermsBootstrap { /** * A scheduler adapter for the platform */ - private final SchedulerAdapter schedulerAdapter; + private final SpongeSchedulerAdapter schedulerAdapter; /** * The plugin class path appender @@ -115,13 +100,12 @@ public class LPSpongeBootstrap implements LuckPermsBootstrap { /** * Reference to the central {@link Game} instance in the API */ - @Inject - private Game game; + private final Game game; /** - * Reference to the sponge scheduler + * Injected plugin container for the plugin */ - private final Scheduler spongeScheduler; + private final PluginContainer pluginContainer; /** * Injected configuration directory for the plugin @@ -130,30 +114,34 @@ public class LPSpongeBootstrap implements LuckPermsBootstrap { @ConfigDir(sharedRoot = false) private Path configDirectory; - /** - * Injected plugin container for the plugin - */ - @Inject - private PluginContainer pluginContainer; + public LPSpongeBootstrap(Supplier loader) { + this.loader = loader; - @Inject - public LPSpongeBootstrap(Logger logger, @SynchronousExecutor SpongeExecutorService syncExecutor, @AsynchronousExecutor SpongeExecutorService asyncExecutor) { - this.logger = new Slf4jPluginLogger(logger); - this.spongeScheduler = Sponge.getScheduler(); - this.schedulerAdapter = new SpongeSchedulerAdapter(this, this.spongeScheduler, syncExecutor, asyncExecutor); - this.classPathAppender = new ReflectionClassPathAppender(this); + Injector injector = loader.get(); + this.logger = new Log4jPluginLogger(injector.getInstance(Logger.class)); + this.game = injector.getInstance(Game.class); + this.pluginContainer = injector.getInstance(PluginContainer.class); + injector.injectMembers(this); + + this.schedulerAdapter = new SpongeSchedulerAdapter(this.game, this.pluginContainer); + this.classPathAppender = new JarInJarClassPathAppender(getClass().getClassLoader()); this.plugin = new LPSpongePlugin(this); } // provide adapters + @Override + public Object getLoader() { + return this.loader; + } + @Override public PluginLogger getPluginLogger() { return this.logger; } @Override - public SchedulerAdapter getScheduler() { + public SpongeSchedulerAdapter getScheduler() { return this.schedulerAdapter; } @@ -164,15 +152,17 @@ public ClassPathAppender getClassPathAppender() { // lifecycle - @Listener(order = Order.FIRST) - public void onEnable(GamePreInitializationEvent event) { - this.startTime = Instant.now(); + @Override + public void onLoad() { try { this.plugin.load(); } finally { this.loadLatch.countDown(); } + } + public void onEnable() { + this.startTime = Instant.now(); try { this.plugin.enable(); } finally { @@ -180,13 +170,7 @@ public void onEnable(GamePreInitializationEvent event) { } } - @Listener(order = Order.LATE) - public void onLateEnable(GamePreInitializationEvent event) { - this.plugin.lateEnable(); - } - - @Listener - public void onDisable(GameStoppingServerEvent event) { + public void onDisable() { this.plugin.disable(); } @@ -207,22 +191,31 @@ public Game getGame() { } public Optional getServer() { - return this.game.isServerAvailable() ? Optional.of(this.game.getServer()) : Optional.empty(); - } - - public Scheduler getSpongeScheduler() { - return this.spongeScheduler; + return this.game.isServerAvailable() ? Optional.of(this.game.server()) : Optional.empty(); } public PluginContainer getPluginContainer() { return this.pluginContainer; } + public void registerListeners(Object obj) { + // Check if we are running Sponge API 9+ + try { + final Method method = org.spongepowered.api.event.EventManager.class.getDeclaredMethod("registerListeners", PluginContainer.class, Object.class, MethodHandles.Lookup.class); + method.invoke(this.game.eventManager(), this.pluginContainer, obj, MethodHandles.lookup()); + return; + } catch (Throwable t) { + // ignore + } + // Fallback to Sponge API 8 + this.game.eventManager().registerListeners(this.pluginContainer, obj); + } + // provide information about the plugin @Override public String getVersion() { - return "@version@"; + return this.pluginContainer.metadata().version().toString(); } @Override @@ -233,25 +226,26 @@ public Instant getStartupTime() { // provide information about the platform @Override - public net.luckperms.api.platform.Platform.Type getType() { - return net.luckperms.api.platform.Platform.Type.SPONGE; + public Platform.Type getType() { + return Platform.Type.SPONGE; } @Override public String getServerBrand() { - return this.game.getPlatform().getContainer(Platform.Component.IMPLEMENTATION).getName(); + PluginMetadata brandMetadata = this.game.platform().container(Component.IMPLEMENTATION).metadata(); + return brandMetadata.name().orElseGet(brandMetadata::id); } @Override public String getServerVersion() { - PluginContainer api = this.game.getPlatform().getContainer(Platform.Component.API); - PluginContainer impl = this.game.getPlatform().getContainer(Platform.Component.IMPLEMENTATION); - return api.getName() + ": " + api.getVersion().orElse("null") + " - " + impl.getName() + ": " + impl.getVersion().orElse("null"); + PluginMetadata api = this.game.platform().container(Component.API).metadata(); + PluginMetadata impl = this.game.platform().container(Component.IMPLEMENTATION).metadata(); + return api.name().orElse("API") + ": " + api.version() + " - " + impl.name().orElse("Impl") + ": " + impl.version(); } @Override public Path getDataDirectory() { - Path dataDirectory = this.game.getGameDirectory().toAbsolutePath().resolve("luckperms"); + Path dataDirectory = this.game.gameDirectory().toAbsolutePath().resolve("luckperms"); try { MoreFiles.createDirectoriesIfNotExists(dataDirectory); } catch (IOException e) { @@ -266,14 +260,19 @@ public Path getConfigDirectory() { } @Override - public Optional getPlayer(UUID uniqueId) { - return getServer().flatMap(s -> s.getPlayer(uniqueId)); + public InputStream getResourceStream(String path) { + return getClass().getClassLoader().getResourceAsStream(path); + } + + @Override + public Optional getPlayer(UUID uniqueId) { + return getServer().flatMap(s -> s.player(uniqueId)); } @Override public Optional lookupUniqueId(String username) { - return getServer().flatMap(server -> server.getGameProfileManager().get(username) - .thenApply(p -> Optional.of(p.getUniqueId())) + return getServer().flatMap(server -> server.gameProfileManager().profile(username) + .thenApply(p -> Optional.of(p.uniqueId())) .exceptionally(x -> Optional.empty()) .join() ); @@ -281,8 +280,8 @@ public Optional lookupUniqueId(String username) { @Override public Optional lookupUsername(UUID uniqueId) { - return getServer().flatMap(server -> server.getGameProfileManager().get(uniqueId) - .thenApply(GameProfile::getName) + return getServer().flatMap(server -> server.gameProfileManager().profile(uniqueId) + .thenApply(GameProfile::name) .exceptionally(x -> Optional.empty()) .join() ); @@ -290,16 +289,16 @@ public Optional lookupUsername(UUID uniqueId) { @Override public int getPlayerCount() { - return getServer().map(server -> server.getOnlinePlayers().size()).orElse(0); + return getServer().map(server -> server.onlinePlayers().size()).orElse(0); } @Override public Collection getPlayerList() { return getServer().map(server -> { - Collection players = server.getOnlinePlayers(); + Collection players = server.onlinePlayers(); List list = new ArrayList<>(players.size()); for (Player player : players) { - list.add(player.getName()); + list.add(player.name()); } return list; }).orElse(Collections.emptyList()); @@ -308,10 +307,10 @@ public Collection getPlayerList() { @Override public Collection getOnlinePlayers() { return getServer().map(server -> { - Collection players = server.getOnlinePlayers(); + Collection players = server.onlinePlayers(); List list = new ArrayList<>(players.size()); for (Player player : players) { - list.add(player.getUniqueId()); + list.add(player.uniqueId()); } return list; }).orElse(Collections.emptyList()); @@ -319,7 +318,7 @@ public Collection getOnlinePlayers() { @Override public boolean isPlayerOnline(UUID uniqueId) { - return getServer().flatMap(server -> server.getPlayer(uniqueId).map(Player::isOnline)).orElse(false); + return getServer().map(server -> server.player(uniqueId).isPresent()).orElse(false); } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java index f3a7b83e0..b1d60eb13 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/LPSpongePlugin.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.calculator.CalculatorFactory; -import me.lucko.luckperms.common.command.abstraction.Command; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; @@ -38,36 +37,40 @@ import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.model.manager.track.StandardTrackManager; import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; +import me.lucko.luckperms.common.sender.AbstractSender; import me.lucko.luckperms.common.sender.DummyConsoleSender; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.tasks.CacheHousekeepingTask; -import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; +import me.lucko.luckperms.common.util.MoreFiles; import me.lucko.luckperms.sponge.calculator.SpongeCalculatorFactory; import me.lucko.luckperms.sponge.commands.SpongeParentCommand; import me.lucko.luckperms.sponge.context.SpongeContextManager; import me.lucko.luckperms.sponge.context.SpongePlayerCalculator; +import me.lucko.luckperms.sponge.listeners.SpongeCommandListUpdater; import me.lucko.luckperms.sponge.listeners.SpongeConnectionListener; import me.lucko.luckperms.sponge.listeners.SpongePlatformListener; import me.lucko.luckperms.sponge.messaging.SpongeMessagingFactory; import me.lucko.luckperms.sponge.model.manager.SpongeGroupManager; import me.lucko.luckperms.sponge.model.manager.SpongeUserManager; import me.lucko.luckperms.sponge.service.LuckPermsService; -import me.lucko.luckperms.sponge.service.ProxyFactory; -import me.lucko.luckperms.sponge.service.model.LPPermissionService; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; -import me.lucko.luckperms.sponge.service.model.ProxiedServiceObject; import me.lucko.luckperms.sponge.service.model.persisted.PersistedCollection; import me.lucko.luckperms.sponge.tasks.ServiceCacheHousekeepingTask; - import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.luckperms.api.LuckPerms; import net.luckperms.api.query.QueryOptions; - -import org.spongepowered.api.service.permission.PermissionDescription; +import org.spongepowered.api.command.Command; +import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.lifecycle.ProvideServiceEvent; +import org.spongepowered.api.event.lifecycle.RegisterCommandEvent; +import org.spongepowered.api.service.context.ContextService; import org.spongepowered.api.service.permission.PermissionService; +import org.spongepowered.plugin.PluginContainer; -import java.util.Collection; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -90,8 +93,6 @@ public class LPSpongePlugin extends AbstractLuckPermsPlugin { private SpongeContextManager contextManager; private LuckPermsService service; - private boolean lateLoad = false; - public LPSpongePlugin(LPSpongeBootstrap bootstrap) { this.bootstrap = bootstrap; } @@ -109,8 +110,11 @@ protected void setupSenderFactory() { @Override protected Set getGlobalDependencies() { Set dependencies = super.getGlobalDependencies(); - dependencies.add(Dependency.ADVENTURE_PLATFORM); + + //dependencies.add(Dependency.ADVENTURE_PLATFORM); //dependencies.add(Dependency.ADVENTURE_PLATFORM_SPONGEAPI); + dependencies.remove(Dependency.ADVENTURE); + dependencies.add(Dependency.CONFIGURATE_CORE); dependencies.add(Dependency.CONFIGURATE_HOCON); dependencies.add(Dependency.HOCON_CONFIG); @@ -119,14 +123,14 @@ protected Set getGlobalDependencies() { @Override protected ConfigurationAdapter provideConfigurationAdapter() { - return new SpongeConfigAdapter(this, resolveConfig("luckperms.conf")); + return new SpongeConfigAdapter(this, resolveConfig()); } @Override protected void registerPlatformListeners() { this.connectionListener = new SpongeConnectionListener(this); - this.bootstrap.getGame().getEventManager().registerListeners(this.bootstrap, this.connectionListener); - this.bootstrap.getGame().getEventManager().registerListeners(this.bootstrap, new SpongePlatformListener(this)); + this.bootstrap.registerListeners(this.connectionListener); + this.bootstrap.registerListeners(new SpongePlatformListener(this)); } @Override @@ -137,7 +141,22 @@ protected MessagingFactory provideMessagingFactory() { @Override protected void registerCommands() { this.commandManager = new SpongeCommandExecutor(this); - this.bootstrap.getGame().getCommandManager().register(this.bootstrap, this.commandManager, "luckperms", "lp", "perm", "perms", "permission", "permissions"); + this.bootstrap.registerListeners(new RegisterCommandsListener(this.bootstrap.getPluginContainer(), this.commandManager)); + } + + public static final class RegisterCommandsListener { + private final PluginContainer pluginContainer; + private final Command.Raw command; + + RegisterCommandsListener(PluginContainer pluginContainer, Command.Raw command) { + this.pluginContainer = pluginContainer; + this.command = command; + } + + @Listener + public void onCommandRegister(RegisterCommandEvent event) { + event.register(this.pluginContainer, this.command, "luckperms", "lp", "perm", "perms", "permission", "permissions"); + } } @Override @@ -156,36 +175,46 @@ protected CalculatorFactory provideCalculatorFactory() { protected void setupContextManager() { this.contextManager = new SpongeContextManager(this); - SpongePlayerCalculator playerCalculator = new SpongePlayerCalculator(this, getConfiguration().get(ConfigKeys.DISABLED_CONTEXTS)); - this.bootstrap.getGame().getEventManager().registerListeners(this.bootstrap, playerCalculator); + SpongePlayerCalculator playerCalculator = new SpongePlayerCalculator(this); + this.bootstrap.registerListeners(playerCalculator); this.contextManager.registerCalculator(playerCalculator); } @Override protected void setupPlatformHooks() { - getLogger().info("Registering PermissionService..."); this.service = new LuckPermsService(this); - PermissionService oldService = this.bootstrap.getGame().getServiceManager().provide(PermissionService.class).orElse(null); - if (oldService != null && !(oldService instanceof ProxiedServiceObject)) { + //PermissionService oldService = this.bootstrap.getGame().getServiceManager().provide(PermissionService.class).orElse(null); + //if (oldService != null && !(oldService instanceof ProxiedServiceObject)) { + // + // // before registering our permission service, copy any existing permission descriptions + // Collection permissionDescriptions = oldService.getDescriptions(); + // for (PermissionDescription description : permissionDescriptions) { + // if (description instanceof ProxiedServiceObject) { + // continue; + // } + // ProxyFactory.registerDescription(this.service, description); + // } + //} + + this.bootstrap.registerListeners(new RegisterServiceListener(this.service)); + } + + public static final class RegisterServiceListener { + private final LuckPermsService service; - // before registering our permission service, copy any existing permission descriptions - Collection permissionDescriptions = oldService.getDescriptions(); - for (PermissionDescription description : permissionDescriptions) { - if (description instanceof ProxiedServiceObject) { - continue; - } - ProxyFactory.registerDescription(this.service, description); - } + RegisterServiceListener(LuckPermsService service) { + this.service = service; } - if (this.bootstrap.getGame().getPluginManager().getPlugin("permissionsex").isPresent()) { - getLogger().warn("Detected PermissionsEx - assuming it's loaded for migration."); - getLogger().warn("Delaying LuckPerms PermissionService registration."); - this.lateLoad = true; - } else { - this.bootstrap.getGame().getServiceManager().setProvider(this.bootstrap, LPPermissionService.class, this.service); - this.bootstrap.getGame().getServiceManager().setProvider(this.bootstrap, PermissionService.class, this.service.sponge()); + @Listener + public void onPermissionServiceProvide(ProvideServiceEvent.EngineScoped event) { + event.suggest(this.service::sponge); + } + + @Listener + public void onContextServiceProvide(ProvideServiceEvent.EngineScoped event) { + event.suggest(this.service::sponge); } } @@ -196,13 +225,25 @@ protected AbstractEventBus provideEventBus(LuckPermsApiProvider apiProvider) @Override protected void registerApiOnPlatform(LuckPerms api) { - this.bootstrap.getGame().getServiceManager().setProvider(this.bootstrap, LuckPerms.class, api); + this.bootstrap.registerListeners(new RegisterApiListener(api)); + } + + public static final class RegisterApiListener { + private final LuckPerms api; + + RegisterApiListener(LuckPerms api) { + this.api = api; + } + + @Listener + public void onLuckPermsServiceProvide(ProvideServiceEvent event) { + event.suggest(() -> this.api); + } } @Override protected void registerHousekeepingTasks() { - this.bootstrap.getScheduler().asyncRepeating(new ExpireTemporaryTask(this), 3, TimeUnit.SECONDS); - this.bootstrap.getScheduler().asyncRepeating(new CacheHousekeepingTask(this), 2, TimeUnit.MINUTES); + super.registerHousekeepingTasks(); this.bootstrap.getScheduler().asyncRepeating(new ServiceCacheHousekeepingTask(this.service), 2, TimeUnit.MINUTES); } @@ -212,13 +253,10 @@ protected void performFinalSetup() { for (CommandPermission perm : CommandPermission.values()) { this.service.registerPermissionDescription(perm.getPermission(), null, this.bootstrap.getPluginContainer()); } - } - public void lateEnable() { - if (this.lateLoad) { - getLogger().info("Providing late registration of PermissionService..."); - this.bootstrap.getGame().getServiceManager().setProvider(this.bootstrap, LPPermissionService.class, this.service); - this.bootstrap.getGame().getServiceManager().setProvider(this.bootstrap, PermissionService.class, this.service.sponge()); + // register sponge command list updater + if (getConfiguration().get(ConfigKeys.UPDATE_CLIENT_COMMAND_LIST)) { + getApiProvider().getEventBus().subscribe(new SpongeCommandListUpdater(this)); } } @@ -232,6 +270,22 @@ public void performPlatformDataSync() { this.service.invalidateAllCaches(); } + private Path resolveConfig() { + Path path = this.bootstrap.getConfigDirectory().resolve("luckperms.conf"); + if (!Files.exists(path)) { + try { + MoreFiles.createDirectoriesIfNotExists(this.bootstrap.getConfigDirectory()); + try (InputStream is = getClass().getClassLoader().getResourceAsStream("luckperms.conf")) { + Files.copy(is, path); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + return path; + } + @Override public Optional getQueryOptionsForUser(User user) { return this.bootstrap.getPlayer(user.getUniqueId()).map(player -> this.contextManager.getQueryOptions(player)); @@ -241,26 +295,28 @@ public Optional getQueryOptionsForUser(User user) { public Stream getOnlineSenders() { return Stream.concat( Stream.of(getConsoleSender()), - this.bootstrap.getServer().map(server -> server.getOnlinePlayers().stream().map(s -> this.senderFactory.wrap(s))).orElseGet(Stream::empty) + this.bootstrap.getServer().map(server -> server.onlinePlayers().stream().map(s -> this.senderFactory.wrap(s))).orElseGet(Stream::empty) ); } @Override public Sender getConsoleSender() { if (this.bootstrap.getGame().isServerAvailable()) { - return this.senderFactory.wrap(this.bootstrap.getGame().getServer().getConsole()); + return this.senderFactory.wrap(this.bootstrap.getGame().systemSubject()); } else { return new DummyConsoleSender(this) { @Override public void sendMessage(Component message) { - LPSpongePlugin.this.getLogger().info(LegacyComponentSerializer.legacySection().serialize(TranslationManager.render(message))); + for (Component line : AbstractSender.splitNewlines(TranslationManager.render(message))) { + LPSpongePlugin.this.bootstrap.getPluginLogger().info(PlainTextComponentSerializer.plainText().serialize(line)); + } } }; } } @Override - public List> getExtraCommands() { + public List> getExtraCommands() { return Collections.singletonList(new SpongeParentCommand(this)); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeCommandExecutor.java b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeCommandExecutor.java index 92b04bd60..3d4ea5654 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeCommandExecutor.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeCommandExecutor.java @@ -29,24 +29,22 @@ import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.sender.Sender; - +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.spongepowered.api.command.CommandCallable; +import org.spongepowered.api.command.Command; +import org.spongepowered.api.command.CommandCause; +import org.spongepowered.api.command.CommandCompletion; import org.spongepowered.api.command.CommandResult; -import org.spongepowered.api.command.CommandSource; +import org.spongepowered.api.command.parameter.ArgumentReader; +import org.spongepowered.api.command.selector.Selector; import org.spongepowered.api.entity.living.player.Player; -import org.spongepowered.api.text.Text; -import org.spongepowered.api.text.selector.Selector; -import org.spongepowered.api.world.Location; -import org.spongepowered.api.world.World; import java.util.List; import java.util.ListIterator; import java.util.Optional; import java.util.stream.Collectors; -public class SpongeCommandExecutor extends CommandManager implements CommandCallable { +public class SpongeCommandExecutor extends CommandManager implements Command.Raw { private final LPSpongePlugin plugin; public SpongeCommandExecutor(LPSpongePlugin plugin) { @@ -55,41 +53,44 @@ public SpongeCommandExecutor(LPSpongePlugin plugin) { } @Override - public @NonNull CommandResult process(@NonNull CommandSource source, @NonNull String args) { - Sender wrapped = this.plugin.getSenderFactory().wrap(source); - List arguments = resolveSelectors(source, ArgumentTokenizer.EXECUTE.tokenizeInput(args)); + public @NonNull CommandResult process(@NonNull CommandCause source, ArgumentReader.@NonNull Mutable args) { + Sender wrapped = this.plugin.getSenderFactory().wrap(source.audience()); + List arguments = resolveSelectors(source, ArgumentTokenizer.EXECUTE.tokenizeInput(args.input())); executeCommand(wrapped, "lp", arguments); return CommandResult.success(); } @Override - public @NonNull List getSuggestions(@NonNull CommandSource source, @NonNull String args, @Nullable Location location) { - Sender wrapped = this.plugin.getSenderFactory().wrap(source); - List arguments = resolveSelectors(source, ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(args)); - return tabCompleteCommand(wrapped, arguments); + public List complete(@NonNull CommandCause source, ArgumentReader.@NonNull Mutable args) { + Sender wrapped = this.plugin.getSenderFactory().wrap(source.audience()); + List arguments = resolveSelectors(source, ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(args.input())); + return tabCompleteCommand(wrapped, arguments) + .stream() + .map(CommandCompletion::of) + .collect(Collectors.toList()); } @Override - public boolean testPermission(@NonNull CommandSource source) { + public boolean canExecute(CommandCause cause) { return true; // we run permission checks internally } @Override - public @NonNull Optional getShortDescription(@NonNull CommandSource source) { - return Optional.of(Text.of("Manage permissions")); + public Optional shortDescription(CommandCause cause) { + return Optional.of(Component.text("Manage permissions")); } @Override - public @NonNull Optional getHelp(@NonNull CommandSource source) { - return Optional.of(Text.of("Run /luckperms to view usage.")); + public Optional extendedDescription(CommandCause cause) { + return Optional.empty(); } @Override - public @NonNull Text getUsage(@NonNull CommandSource source) { - return Text.of("/luckperms"); + public Component usage(CommandCause cause) { + return Component.text("/luckperms"); } - private List resolveSelectors(CommandSource source, List args) { + private List resolveSelectors(CommandCause source, List args) { if (!this.plugin.getConfiguration().get(ConfigKeys.RESOLVE_COMMAND_SELECTORS)) { return args; } @@ -102,7 +103,7 @@ private List resolveSelectors(CommandSource source, List args) { List matchedPlayers; try { - matchedPlayers = Selector.parse(arg).resolve(source).stream() + matchedPlayers = Selector.parse(arg).select(source).stream() .filter(e -> e instanceof Player) .map(e -> (Player) e) .collect(Collectors.toList()); @@ -122,7 +123,7 @@ private List resolveSelectors(CommandSource source, List args) { } Player player = matchedPlayers.get(0); - it.set(player.getUniqueId().toString()); + it.set(player.uniqueId().toString()); } return args; diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeConfigAdapter.java b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeConfigAdapter.java index 592f20cd4..6e517bab8 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeConfigAdapter.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeConfigAdapter.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.config.generic.adapter.ConfigurateConfigAdapter; import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.hocon.HoconConfigurationLoader; import ninja.leaping.configurate.loader.ConfigurationLoader; diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeEventBus.java b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeEventBus.java index 55e498d09..4c00b4c4a 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeEventBus.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeEventBus.java @@ -27,17 +27,12 @@ import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.event.AbstractEventBus; - import org.spongepowered.api.Sponge; -import org.spongepowered.api.plugin.PluginContainer; +import org.spongepowered.plugin.PluginContainer; public class SpongeEventBus extends AbstractEventBus { public SpongeEventBus(LPSpongePlugin plugin, LuckPermsApiProvider apiProvider) { super(plugin, apiProvider); - - // register listener - LPSpongeBootstrap bootstrap = plugin.getBootstrap(); - bootstrap.getGame().getEventManager().registerListeners(bootstrap, this); } @Override @@ -46,7 +41,7 @@ protected PluginContainer checkPlugin(Object plugin) throws IllegalArgumentExcep return (PluginContainer) plugin; } - PluginContainer pluginContainer = Sponge.getPluginManager().fromInstance(plugin).orElse(null); + PluginContainer pluginContainer = Sponge.pluginManager().fromInstance(plugin).orElse(null); if (pluginContainer != null) { return pluginContainer; } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeSchedulerAdapter.java b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeSchedulerAdapter.java index 4d5e5e318..24ca4d073 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeSchedulerAdapter.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeSchedulerAdapter.java @@ -25,84 +25,91 @@ package me.lucko.luckperms.sponge; +import com.google.common.base.Suppliers; import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; +import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Iterators; - +import org.spongepowered.api.Game; +import org.spongepowered.api.scheduler.ScheduledTask; import org.spongepowered.api.scheduler.Scheduler; -import org.spongepowered.api.scheduler.SpongeExecutorService; import org.spongepowered.api.scheduler.Task; +import org.spongepowered.api.scheduler.TaskExecutorService; +import org.spongepowered.plugin.PluginContainer; import java.util.Collections; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; public class SpongeSchedulerAdapter implements SchedulerAdapter { - private final LPSpongeBootstrap bootstrap; - - private final Scheduler scheduler; - private final SpongeExecutorService sync; - private final SpongeExecutorService async; + + private final Game game; + private final PluginContainer pluginContainer; + + private final Scheduler asyncScheduler; + private final Supplier sync; + private final TaskExecutorService async; - private final Set tasks = Collections.newSetFromMap(new WeakHashMap<>()); + private final Set tasks = Collections.newSetFromMap(new WeakHashMap<>()); - public SpongeSchedulerAdapter(LPSpongeBootstrap bootstrap, Scheduler scheduler, SpongeExecutorService sync, SpongeExecutorService async) { - this.bootstrap = bootstrap; - this.scheduler = scheduler; - this.sync = sync; - this.async = async; + public SpongeSchedulerAdapter(Game game, PluginContainer pluginContainer) { + this.game = game; + this.pluginContainer = pluginContainer; + + this.asyncScheduler = game.asyncScheduler(); + this.async = this.asyncScheduler.executor(pluginContainer); + this.sync = Suppliers.memoize(() -> getSyncScheduler().executor(this.pluginContainer)); } - @Override - public Executor async() { - return this.async; + public Scheduler getSyncScheduler() { + return this.game.server().scheduler(); } - @Override - public Executor sync() { - return this.sync; + public void executeSync(Runnable task) { + this.sync.get().execute(task); } @Override - public void executeAsync(Runnable runnable) { - this.scheduler.createTaskBuilder().async().execute(runnable).submit(this.bootstrap); + public void executeSync(Sender ctx, Runnable task) { + this.sync.get().execute(task); } @Override - public void executeSync(Runnable runnable) { - this.scheduler.createTaskBuilder().execute(runnable).submit(this.bootstrap); + public Executor async() { + return this.async; + } + + private SchedulerTask submitAsyncTask(Runnable runnable, Consumer config) { + Task.Builder builder = Task.builder(); + config.accept(builder); + + Task task = builder + .execute(runnable) + .plugin(this.pluginContainer) + .build(); + + ScheduledTask scheduledTask = this.asyncScheduler.submit(task); + this.tasks.add(scheduledTask); + return scheduledTask::cancel; } @Override public SchedulerTask asyncLater(Runnable task, long delay, TimeUnit unit) { - Task t = this.scheduler.createTaskBuilder() - .async() - .delay(delay, unit) - .execute(task) - .submit(this.bootstrap); - - this.tasks.add(t); - return t::cancel; + return submitAsyncTask(task, builder -> builder.delay(delay, unit)); } @Override public SchedulerTask asyncRepeating(Runnable task, long interval, TimeUnit unit) { - Task t = this.scheduler.createTaskBuilder() - .async() - .interval(interval, unit) - .delay(interval, unit) - .execute(task) - .submit(this.bootstrap); - - this.tasks.add(t); - return t::cancel; + return submitAsyncTask(task, builder -> builder.delay(interval, unit).interval(interval, unit)); } @Override public void shutdownScheduler() { - Iterators.tryIterate(this.tasks, Task::cancel); + Iterators.tryIterate(this.tasks, ScheduledTask::cancel); } @Override diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeSenderFactory.java b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeSenderFactory.java index 7f7c9a517..ac585a945 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeSenderFactory.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/SpongeSenderFactory.java @@ -29,51 +29,60 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.sender.SenderFactory; import me.lucko.luckperms.sponge.service.CompatibilityUtil; - +import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.luckperms.api.util.Tristate; - -import org.spongepowered.api.command.CommandSource; -import org.spongepowered.api.command.source.ConsoleSource; +import org.spongepowered.api.SystemSubject; +import org.spongepowered.api.command.exception.CommandException; import org.spongepowered.api.entity.living.player.Player; -import org.spongepowered.api.text.Text; -import org.spongepowered.api.text.serializer.TextSerializers; +import org.spongepowered.api.service.permission.Subject; +import java.util.Locale; import java.util.UUID; -public class SpongeSenderFactory extends SenderFactory { +public class SpongeSenderFactory extends SenderFactory { public SpongeSenderFactory(LPSpongePlugin plugin) { super(plugin); } @Override - protected String getName(CommandSource source) { + protected String getName(Audience source) { if (source instanceof Player) { - return source.getName(); + return ((Player) source).name(); } return Sender.CONSOLE_NAME; } @Override - protected UUID getUniqueId(CommandSource source) { + protected UUID getUniqueId(Audience source) { if (source instanceof Player) { - return ((Player) source).getUniqueId(); + return ((Player) source).uniqueId(); } return Sender.CONSOLE_UUID; } @Override - protected void sendMessage(CommandSource source, Component message) { - source.sendMessage(toNativeText(TranslationManager.render(message, source.getLocale()))); + protected void sendMessage(Audience source, Component message) { + Locale locale = null; + if (source instanceof Player) { + locale = ((Player) source).locale(); + } + Component rendered = TranslationManager.render(message, locale); + + source.sendMessage(rendered); } @Override - protected Tristate getPermissionValue(CommandSource source, String node) { - Tristate result = CompatibilityUtil.convertTristate(source.getPermissionValue(source.getActiveContexts(), node)); + protected Tristate getPermissionValue(Audience source, String node) { + if (!(source instanceof Subject)) { + throw new IllegalStateException("Source is not a subject"); + } + + final Subject subject = (Subject) source; + Tristate result = CompatibilityUtil.convertTristate(subject.permissionValue(node)); // check the permdefault - if (result == Tristate.UNDEFINED && source.hasPermission(node)) { + if (result == Tristate.UNDEFINED && subject.hasPermission(node)) { result = Tristate.TRUE; } @@ -81,22 +90,30 @@ protected Tristate getPermissionValue(CommandSource source, String node) { } @Override - protected boolean hasPermission(CommandSource source, String node) { - return source.hasPermission(node); - } + protected boolean hasPermission(Audience source, String node) { + if (!(source instanceof Subject)) { + throw new IllegalStateException("Source is not a subject"); + } - @Override - protected void performCommand(CommandSource source, String command) { - getPlugin().getBootstrap().getGame().getCommandManager().process(source, command); + final Subject subject = (Subject) source; + return subject.hasPermission(node); } @Override - protected boolean isConsole(CommandSource sender) { - return sender instanceof ConsoleSource; - } + protected void performCommand(Audience source, String command) { + if (!(source instanceof Subject)) { + throw new IllegalStateException("Source is not a subject"); + } - public static Text toNativeText(Component component) { - return TextSerializers.JSON.deserialize(GsonComponentSerializer.gson().serialize(component)); + try { + getPlugin().getBootstrap().getGame().server().commandManager().process(((Subject) source), source, command); + } catch (CommandException e) { + // ignore + } } + @Override + protected boolean isConsole(Audience sender) { + return sender instanceof SystemSubject; + } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/DefaultsProcessor.java b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/DefaultsProcessor.java deleted file mode 100644 index 0a5bd8f9a..000000000 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/DefaultsProcessor.java +++ /dev/null @@ -1,91 +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.sponge.calculator; - -import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; -import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; -import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; -import me.lucko.luckperms.common.calculator.result.TristateResult; -import me.lucko.luckperms.sponge.service.model.LPPermissionService; -import me.lucko.luckperms.sponge.service.model.LPSubject; - -import net.luckperms.api.query.QueryOptions; -import net.luckperms.api.util.Tristate; - -public abstract class DefaultsProcessor implements PermissionProcessor { - private static final TristateResult.Factory TYPE_DEFAULTS_RESULT_FACTORY = new TristateResult.Factory(DefaultsProcessor.class, "type defaults"); - private static final TristateResult.Factory ROOT_DEFAULTS_RESULT_FACTORY = new TristateResult.Factory(DefaultsProcessor.class, "root defaults"); - - protected final LPPermissionService service; - private final QueryOptions queryOptions; - private final boolean overrideWildcards; - - public DefaultsProcessor(LPPermissionService service, QueryOptions queryOptions, boolean overrideWildcards) { - this.service = service; - this.queryOptions = queryOptions; - this.overrideWildcards = overrideWildcards; - } - - protected abstract LPSubject getTypeDefaults(); - - private boolean canOverrideWildcard(TristateResult prev) { - return this.overrideWildcards && - (prev.processorClass() == WildcardProcessor.class || prev.processorClass() == SpongeWildcardProcessor.class) && - prev.result() == Tristate.TRUE; - } - - @Override - public TristateResult hasPermission(TristateResult prev, String permission) { - if (prev != TristateResult.UNDEFINED) { - // Check to see if the result should be overridden - if (canOverrideWildcard(prev)) { - Tristate t = getTypeDefaults().getPermissionValue(this.queryOptions, permission); - if (t == Tristate.FALSE) { - return TYPE_DEFAULTS_RESULT_FACTORY.result(Tristate.FALSE, "type defaults (overriding wildcard): " + prev.cause()); - } - - t = this.service.getRootDefaults().getPermissionValue(this.queryOptions, permission); - if (t == Tristate.FALSE) { - return ROOT_DEFAULTS_RESULT_FACTORY.result(Tristate.FALSE, "root defaults (overriding wildcard): " + prev.cause()); - } - } - - return prev; - } - - Tristate t = getTypeDefaults().getPermissionValue(this.queryOptions, permission); - if (t != Tristate.UNDEFINED) { - return TYPE_DEFAULTS_RESULT_FACTORY.result(t); - } - - t = this.service.getRootDefaults().getPermissionValue(this.queryOptions, permission); - if (t != Tristate.UNDEFINED) { - return ROOT_DEFAULTS_RESULT_FACTORY.result(t); - } - - return TristateResult.UNDEFINED; - } -} diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/FixedDefaultsProcessor.java b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/FixedTypeDefaultsProcessor.java similarity index 88% rename from sponge/src/main/java/me/lucko/luckperms/sponge/calculator/FixedDefaultsProcessor.java rename to sponge/src/main/java/me/lucko/luckperms/sponge/calculator/FixedTypeDefaultsProcessor.java index 7fb466d22..0a6ef248c 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/FixedDefaultsProcessor.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/FixedTypeDefaultsProcessor.java @@ -27,13 +27,12 @@ import me.lucko.luckperms.sponge.service.model.LPPermissionService; import me.lucko.luckperms.sponge.service.model.LPSubject; - import net.luckperms.api.query.QueryOptions; -public class FixedDefaultsProcessor extends DefaultsProcessor { +public class FixedTypeDefaultsProcessor extends TypeDefaultsProcessor { private final LPSubject defaultsSubject; - public FixedDefaultsProcessor(LPPermissionService service, QueryOptions queryOptions, LPSubject defaultsSubject, boolean overrideWildcards) { + public FixedTypeDefaultsProcessor(LPPermissionService service, QueryOptions queryOptions, LPSubject defaultsSubject, boolean overrideWildcards) { super(service, queryOptions, overrideWildcards); this.defaultsSubject = defaultsSubject; } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/GroupDefaultsProcessor.java b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/GroupTypeDefaultsProcessor.java similarity index 88% rename from sponge/src/main/java/me/lucko/luckperms/sponge/calculator/GroupDefaultsProcessor.java rename to sponge/src/main/java/me/lucko/luckperms/sponge/calculator/GroupTypeDefaultsProcessor.java index 8a4fb8050..c3c03dee3 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/GroupDefaultsProcessor.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/GroupTypeDefaultsProcessor.java @@ -28,11 +28,10 @@ import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import me.lucko.luckperms.sponge.service.model.LPPermissionService; import me.lucko.luckperms.sponge.service.model.LPSubject; - import net.luckperms.api.query.QueryOptions; -public class GroupDefaultsProcessor extends DefaultsProcessor implements PermissionProcessor { - public GroupDefaultsProcessor(LPPermissionService service, QueryOptions queryOptions, boolean overrideWildcards) { +public class GroupTypeDefaultsProcessor extends TypeDefaultsProcessor implements PermissionProcessor { + public GroupTypeDefaultsProcessor(LPPermissionService service, QueryOptions queryOptions, boolean overrideWildcards) { super(service, queryOptions, overrideWildcards); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/RootDefaultsProcessor.java b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/RootDefaultsProcessor.java new file mode 100644 index 000000000..d87128b3d --- /dev/null +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/RootDefaultsProcessor.java @@ -0,0 +1,56 @@ +/* + * 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.sponge.calculator; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractOverrideWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.sponge.service.model.LPPermissionService; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; + +public class RootDefaultsProcessor extends AbstractOverrideWildcardProcessor implements PermissionProcessor { + private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(RootDefaultsProcessor.class); + + protected final LPPermissionService service; + private final QueryOptions queryOptions; + + public RootDefaultsProcessor(LPPermissionService service, QueryOptions queryOptions, boolean overrideWildcards) { + super(overrideWildcards); + this.service = service; + this.queryOptions = queryOptions; + } + + @Override + public TristateResult hasPermission(String permission) { + Tristate t = this.service.getRootDefaults().getPermissionValue(this.queryOptions, permission); + if (t != Tristate.UNDEFINED) { + return RESULT_FACTORY.result(t); + } + + return TristateResult.UNDEFINED; + } +} diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/SpongeCalculatorFactory.java b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/SpongeCalculatorFactory.java index 89ade487a..68284ec6e 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/SpongeCalculatorFactory.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/SpongeCalculatorFactory.java @@ -28,6 +28,7 @@ import me.lucko.luckperms.common.cacheddata.CacheMetadata; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorMonitored; import me.lucko.luckperms.common.calculator.processor.DirectProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import me.lucko.luckperms.common.calculator.processor.RegexProcessor; @@ -36,11 +37,12 @@ import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.model.HolderType; import me.lucko.luckperms.sponge.LPSpongePlugin; - +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class SpongeCalculatorFactory implements CalculatorFactory { private final LPSpongePlugin plugin; @@ -50,32 +52,33 @@ public SpongeCalculatorFactory(LPSpongePlugin plugin) { } @Override - public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { + public PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata) { List processors = new ArrayList<>(6); - processors.add(new DirectProcessor()); + processors.add(new DirectProcessor(sourceMap)); if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { - processors.add(new RegexProcessor()); + processors.add(new RegexProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { - processors.add(new WildcardProcessor()); + processors.add(new WildcardProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { - processors.add(new SpongeWildcardProcessor()); + processors.add(new SpongeWildcardProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLY_SPONGE_DEFAULT_SUBJECTS)) { boolean overrideWildcards = this.plugin.getConfiguration().get(ConfigKeys.APPLY_DEFAULT_NEGATIONS_BEFORE_WILDCARDS); if (metadata.getHolderType() == HolderType.USER) { - processors.add(new UserDefaultsProcessor(this.plugin.getService(), queryOptions, overrideWildcards)); + processors.add(new UserTypeDefaultsProcessor(this.plugin.getService(), queryOptions, overrideWildcards)); } else if (metadata.getHolderType() == HolderType.GROUP) { - processors.add(new GroupDefaultsProcessor(this.plugin.getService(), queryOptions, overrideWildcards)); + processors.add(new GroupTypeDefaultsProcessor(this.plugin.getService(), queryOptions, overrideWildcards)); } + processors.add(new RootDefaultsProcessor(this.plugin.getService(), queryOptions, overrideWildcards)); } - return new PermissionCalculator(this.plugin, metadata, processors); + return new PermissionCalculatorMonitored(this.plugin, metadata, processors); } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/TypeDefaultsProcessor.java b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/TypeDefaultsProcessor.java new file mode 100644 index 000000000..373d3a8b5 --- /dev/null +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/TypeDefaultsProcessor.java @@ -0,0 +1,59 @@ +/* + * 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.sponge.calculator; + +import me.lucko.luckperms.common.cacheddata.result.TristateResult; +import me.lucko.luckperms.common.calculator.processor.AbstractOverrideWildcardProcessor; +import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; +import me.lucko.luckperms.sponge.service.model.LPPermissionService; +import me.lucko.luckperms.sponge.service.model.LPSubject; +import net.luckperms.api.query.QueryOptions; +import net.luckperms.api.util.Tristate; + +public abstract class TypeDefaultsProcessor extends AbstractOverrideWildcardProcessor implements PermissionProcessor { + private static final TristateResult.Factory RESULT_FACTORY = new TristateResult.Factory(TypeDefaultsProcessor.class); + + protected final LPPermissionService service; + private final QueryOptions queryOptions; + + public TypeDefaultsProcessor(LPPermissionService service, QueryOptions queryOptions, boolean overrideWildcards) { + super(overrideWildcards); + this.service = service; + this.queryOptions = queryOptions; + } + + protected abstract LPSubject getTypeDefaults(); + + @Override + public TristateResult hasPermission(String permission) { + Tristate t = getTypeDefaults().getPermissionValue(this.queryOptions, permission); + if (t != Tristate.UNDEFINED) { + return RESULT_FACTORY.result(t); + } + + return TristateResult.UNDEFINED; + } +} diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/UserDefaultsProcessor.java b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/UserTypeDefaultsProcessor.java similarity index 88% rename from sponge/src/main/java/me/lucko/luckperms/sponge/calculator/UserDefaultsProcessor.java rename to sponge/src/main/java/me/lucko/luckperms/sponge/calculator/UserTypeDefaultsProcessor.java index 30e02cd4e..fcb874481 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/UserDefaultsProcessor.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/calculator/UserTypeDefaultsProcessor.java @@ -28,11 +28,10 @@ import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import me.lucko.luckperms.sponge.service.model.LPPermissionService; import me.lucko.luckperms.sponge.service.model.LPSubject; - import net.luckperms.api.query.QueryOptions; -public class UserDefaultsProcessor extends DefaultsProcessor implements PermissionProcessor { - public UserDefaultsProcessor(LPPermissionService service, QueryOptions queryOptions, boolean overrideWildcards) { +public class UserTypeDefaultsProcessor extends TypeDefaultsProcessor implements PermissionProcessor { + public UserTypeDefaultsProcessor(LPPermissionService service, QueryOptions queryOptions, boolean overrideWildcards) { super(service, queryOptions, overrideWildcards); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionClear.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionClear.java index 07a82264d..6722f92d4 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionClear.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionClear.java @@ -33,7 +33,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; public class OptionClear extends ChildCommand { diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionInfo.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionInfo.java index 655da0e72..585e05175 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionInfo.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionInfo.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.sponge.commands; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; @@ -35,7 +34,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; import java.util.Map; diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionSet.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionSet.java index c634b6792..ab133562a 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionSet.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionSet.java @@ -33,7 +33,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; public class OptionSet extends ChildCommand { diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionUnset.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionUnset.java index 58833481d..be3a939d2 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionUnset.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/OptionUnset.java @@ -33,7 +33,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; public class OptionUnset extends ChildCommand { diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentAdd.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentAdd.java index f42a5e092..586036369 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentAdd.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentAdd.java @@ -36,11 +36,8 @@ import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; -import org.spongepowered.api.Sponge; - public class ParentAdd extends ChildCommand { public ParentAdd() { super(CommandSpec.SPONGE_PARENT_ADD, "add", CommandPermission.SPONGE_PARENT_ADD, Predicates.inRange(0, 1)); @@ -52,7 +49,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, LPSubjectData subject String name = args.get(1); ImmutableContextSet contextSet = args.getContextOrEmpty(2); - LPPermissionService service = Sponge.getServiceManager().provideUnchecked(LPPermissionService.class); + LPPermissionService service = subjectData.getParentSubject().getService(); if (service.getLoadedCollections().keySet().stream().map(String::toLowerCase).noneMatch(s -> s.equalsIgnoreCase(collection))) { SpongeCommandUtils.sendPrefixed(sender, "Warning: SubjectCollection '&4" + collection + "&c' doesn't already exist."); } @@ -66,7 +63,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, LPSubjectData subject if (subjectData.addParent(contextSet, subject.toReference()).join()) { SpongeCommandUtils.sendPrefixed(sender, "&aAdded parent &b" + subject.getParentCollection().getIdentifier() + - "&a/&b" + subject.getIdentifier() + "&a in context " + SpongeCommandUtils.contextToString(contextSet)); + "&a/&b" + subject.getIdentifier().getName() + "&a in context " + SpongeCommandUtils.contextToString(contextSet)); } else { SpongeCommandUtils.sendPrefixed(sender, "Unable to add parent. Does the Subject already have it added?"); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentClear.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentClear.java index 7e391b1b3..340316a92 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentClear.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentClear.java @@ -33,7 +33,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; public class ParentClear extends ChildCommand { diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentInfo.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentInfo.java index 416ee30a9..52915fa33 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentInfo.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentInfo.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.sponge.commands; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; @@ -36,7 +35,6 @@ import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.model.LPSubjectData; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import net.luckperms.api.context.ImmutableContextSet; import java.util.List; diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentRemove.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentRemove.java index a3d3ece54..e35d834f7 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentRemove.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/ParentRemove.java @@ -36,11 +36,8 @@ import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; -import org.spongepowered.api.Sponge; - public class ParentRemove extends ChildCommand { public ParentRemove() { super(CommandSpec.SPONGE_PARENT_REMOVE, "remove", CommandPermission.SPONGE_PARENT_REMOVE, Predicates.inRange(0, 1)); @@ -52,7 +49,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, LPSubjectData subject String name = args.get(1); ImmutableContextSet contextSet = args.getContextOrEmpty(2); - LPPermissionService service = Sponge.getServiceManager().provideUnchecked(LPPermissionService.class); + LPPermissionService service = subjectData.getParentSubject().getService(); if (service.getLoadedCollections().keySet().stream().map(String::toLowerCase).noneMatch(s -> s.equalsIgnoreCase(collection))) { SpongeCommandUtils.sendPrefixed(sender, "Warning: SubjectCollection '&4" + collection + "&c' doesn't exist."); } @@ -66,7 +63,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, LPSubjectData subject if (subjectData.removeParent(contextSet, subject.toReference()).join()) { SpongeCommandUtils.sendPrefixed(sender, "&aRemoved parent &b" + subject.getParentCollection().getIdentifier() + - "&a/&b" + subject.getIdentifier() + "&a in context " + SpongeCommandUtils.contextToString(contextSet)); + "&a/&b" + subject.getIdentifier().getName() + "&a in context " + SpongeCommandUtils.contextToString(contextSet)); } else { SpongeCommandUtils.sendPrefixed(sender, "Unable to remove parent. Are you sure the Subject has it added?"); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionClear.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionClear.java index 641134493..229abed1d 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionClear.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionClear.java @@ -33,7 +33,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; public class PermissionClear extends ChildCommand { diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionInfo.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionInfo.java index a045af377..1ff407810 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionInfo.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionInfo.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.sponge.commands; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.access.CommandPermission; import me.lucko.luckperms.common.command.spec.CommandSpec; @@ -35,7 +34,6 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; import java.util.Map; diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionSet.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionSet.java index 2a559ac5d..cc9fc5ae1 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionSet.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/PermissionSet.java @@ -34,10 +34,11 @@ import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.util.Tristate; +import java.util.Locale; + public class PermissionSet extends ChildCommand { public PermissionSet() { super(CommandSpec.SPONGE_PERMISSION_SET, "set", CommandPermission.SPONGE_PERMISSION_SET, Predicates.inRange(0, 1)); @@ -50,7 +51,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, LPSubjectData subject ImmutableContextSet contextSet = args.getContextOrEmpty(2); if (subjectData.setPermission(contextSet, node, tristate).join()) { - SpongeCommandUtils.sendPrefixed(sender, "&aSet &b" + node + "&a to &b" + tristate.toString().toLowerCase() + "&a in context " + SpongeCommandUtils.contextToString(contextSet)); + SpongeCommandUtils.sendPrefixed(sender, "&aSet &b" + node + "&a to &b" + tristate.toString().toLowerCase(Locale.ROOT) + "&a in context " + SpongeCommandUtils.contextToString(contextSet)); } else { SpongeCommandUtils.sendPrefixed(sender, "Unable to set permission. Does the Subject already have it set?"); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/SpongeCommandUtils.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/SpongeCommandUtils.java index 501829e7b..8efd4a49b 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/SpongeCommandUtils.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/SpongeCommandUtils.java @@ -30,12 +30,12 @@ import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.luckperms.api.context.ContextSet; import net.luckperms.api.util.Tristate; import java.util.Collection; +import java.util.Locale; import java.util.Map; public final class SpongeCommandUtils { @@ -46,7 +46,7 @@ public static void sendPrefixed(Sender sender, String message) { } public static Tristate parseTristate(int index, ArgumentList args) throws ArgumentException { - String s = args.get(index).toLowerCase(); + String s = args.get(index).toLowerCase(Locale.ROOT); switch (s) { case "1": case "true": @@ -93,9 +93,9 @@ public static String parentsToString(Iterable parents) { StringBuilder sb = new StringBuilder(); for (LPSubjectReference s : parents) { sb.append("&3> &a") - .append(s.getSubjectIdentifier()) + .append(s.subjectIdentifier()) .append(" &bfrom collection &a") - .append(s.getCollectionIdentifier()) + .append(s.collectionIdentifier()) .append("&b.\n"); } return sb.toString(); diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/SpongeParentCommand.java b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/SpongeParentCommand.java index fd9eff857..8a4768581 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/commands/SpongeParentCommand.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/commands/SpongeParentCommand.java @@ -27,7 +27,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.command.abstraction.ChildCommand; import me.lucko.luckperms.common.command.abstraction.Command; import me.lucko.luckperms.common.command.abstraction.CommandException; @@ -45,6 +44,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; @@ -110,7 +110,7 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Void v, ArgumentList if (args.size() < 2) { List subjects = collection.getLoadedSubjects().stream() - .map(LPSubject::getIdentifier) + .map(lpSubject -> lpSubject.getIdentifier().getName()) .collect(Collectors.toList()); if (subjects.size() > 50) { @@ -131,12 +131,12 @@ public void execute(LuckPermsPlugin plugin, Sender sender, Void v, ArgumentList } boolean persistent = true; - if (args.get(2).toLowerCase().startsWith("-t")) { + if (args.get(2).toLowerCase(Locale.ROOT).startsWith("-t")) { persistent = false; args.remove(2); } - String type = args.get(2).toLowerCase(); + String type = args.get(2).toLowerCase(Locale.ROOT); if (!type.equals("permission") && !type.equals("parent") && !type.equals("option")) { sendDetailedUsage(sender, label); return; @@ -187,7 +187,7 @@ public void sendUsage(Sender sender, String label) { public void sendDetailedUsage(Sender sender, String label) { SpongeCommandUtils.sendPrefixed(sender, "&b" + getName() + " Sub Commands: &7(" + String.format("/%s sponge [-transient]", label) + " ...)"); for (String s : Arrays.asList("Permission", "Parent", "Option")) { - List> subs = this.children.get(s.toLowerCase()).stream() + List> subs = this.children.get(s.toLowerCase(Locale.ROOT)).stream() .filter(sub -> sub.isAuthorized(sender)) .collect(Collectors.toList()); diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/context/SpongeContextManager.java b/sponge/src/main/java/me/lucko/luckperms/sponge/context/SpongeContextManager.java index 90b415837..b2d2fcbc1 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/context/SpongeContextManager.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/context/SpongeContextManager.java @@ -25,57 +25,64 @@ package me.lucko.luckperms.sponge.context; -import com.github.benmanes.caffeine.cache.LoadingCache; - -import me.lucko.luckperms.common.context.ContextManager; -import me.lucko.luckperms.common.context.QueryOptionsCache; -import me.lucko.luckperms.common.context.QueryOptionsSupplier; -import me.lucko.luckperms.common.util.CaffeineFactory; +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.context.manager.SimpleContextManager; import me.lucko.luckperms.sponge.LPSpongePlugin; - -import net.luckperms.api.context.ImmutableContextSet; +import me.lucko.luckperms.sponge.service.model.ContextCalculatorProxy; +import me.lucko.luckperms.sponge.service.model.TemporaryCauseHolderSubject; +import net.luckperms.api.context.ContextCalculator; +import net.luckperms.api.context.ContextConsumer; +import net.luckperms.api.context.ContextSet; +import net.luckperms.api.context.StaticContextCalculator; import net.luckperms.api.query.QueryOptions; - -import org.spongepowered.api.entity.living.player.Player; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.event.Cause; import org.spongepowered.api.service.permission.Subject; import java.util.UUID; -import java.util.concurrent.TimeUnit; -public class SpongeContextManager extends ContextManager { - - private final LoadingCache> subjectCaches = CaffeineFactory.newBuilder() - .expireAfterAccess(1, TimeUnit.MINUTES) - .build(key -> new QueryOptionsCache<>(key, this)); +public class SpongeContextManager extends SimpleContextManager { public SpongeContextManager(LPSpongePlugin plugin) { - super(plugin, Subject.class, Player.class); + super(plugin, Subject.class, ServerPlayer.class); } @Override - public UUID getUniqueId(Player player) { - return player.getUniqueId(); - } + protected void callContextCalculator(ContextCalculator calculator, Subject subject, ContextConsumer consumer) { + if (subject instanceof TemporaryCauseHolderSubject) { + Cause cause = ((TemporaryCauseHolderSubject) subject).getCause(); + Subject actualSubject = ((TemporaryCauseHolderSubject) subject).getSubject(); - @Override - public QueryOptionsSupplier getCacheFor(Subject subject) { - if (subject == null) { - throw new NullPointerException("subject"); + if (calculator instanceof ContextCalculatorProxy) { + ((ContextCalculatorProxy) calculator).calculate(cause, consumer); + } else if (actualSubject != null) { + calculator.calculate(actualSubject, consumer); + } else if (calculator instanceof StaticContextCalculator) { + ((StaticContextCalculator) calculator).calculate(consumer); + } /* else { + // we just have to fail... + // there's no way to call a LuckPerms ContextCalculator if a Subject instance + // doesn't exist for the cause. + } */ + } else { + Object associatedObject = subject.associatedObject().orElse(null); + if (associatedObject instanceof Subject) { + calculator.calculate((Subject) associatedObject, consumer); + } else { + calculator.calculate(subject, consumer); + } } - - return this.subjectCaches.get(subject); } @Override - protected void invalidateCache(Subject subject) { - QueryOptionsCache cache = this.subjectCaches.getIfPresent(subject); - if (cache != null) { - cache.invalidate(); - } + public UUID getUniqueId(ServerPlayer player) { + return player.uniqueId(); } - @Override - public QueryOptions formQueryOptions(Subject subject, ImmutableContextSet contextSet) { - return formQueryOptions(contextSet); + public QueryOptions formQueryOptions(ContextSet contexts) { + QueryOptions.Builder builder = this.plugin.getConfiguration().get(ConfigKeys.GLOBAL_QUERY_OPTIONS).toBuilder().context(contexts); + customizeStaticQueryOptions(builder); + return builder.build(); } + } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/context/SpongePlayerCalculator.java b/sponge/src/main/java/me/lucko/luckperms/sponge/context/SpongePlayerCalculator.java index c2103b057..ede7aef76 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/context/SpongePlayerCalculator.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/context/SpongePlayerCalculator.java @@ -26,92 +26,71 @@ package me.lucko.luckperms.sponge.context; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.sponge.LPSpongePlugin; - import net.luckperms.api.context.Context; import net.luckperms.api.context.ContextCalculator; import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; - import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.CatalogType; -import org.spongepowered.api.CatalogTypes; import org.spongepowered.api.Game; -import org.spongepowered.api.command.CommandSource; -import org.spongepowered.api.data.key.Keys; +import org.spongepowered.api.ResourceKey; +import org.spongepowered.api.Server; +import org.spongepowered.api.data.Keys; import org.spongepowered.api.data.value.ValueContainer; import org.spongepowered.api.entity.Entity; -import org.spongepowered.api.entity.living.Humanoid; -import org.spongepowered.api.entity.living.player.gamemode.GameMode; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.Order; -import org.spongepowered.api.event.entity.MoveEntityEvent; -import org.spongepowered.api.event.entity.living.humanoid.ChangeGameModeEvent; +import org.spongepowered.api.event.entity.ChangeEntityWorldEvent; +import org.spongepowered.api.registry.RegistryTypes; import org.spongepowered.api.service.permission.Subject; -import org.spongepowered.api.world.DimensionType; import org.spongepowered.api.world.Locatable; import org.spongepowered.api.world.World; - -import java.util.Set; +import org.spongepowered.api.world.server.ServerWorld; public class SpongePlayerCalculator implements ContextCalculator { private final LPSpongePlugin plugin; - private final boolean gamemode; - private final boolean world; - private final boolean dimensionType; - - public SpongePlayerCalculator(LPSpongePlugin plugin, Set disabled) { + public SpongePlayerCalculator(LPSpongePlugin plugin) { this.plugin = plugin; - this.gamemode = !disabled.contains(DefaultContextKeys.GAMEMODE_KEY); - this.world = !disabled.contains(DefaultContextKeys.WORLD_KEY); - this.dimensionType = !disabled.contains(DefaultContextKeys.DIMENSION_TYPE_KEY); } @Override public void calculate(@NonNull Subject subject, @NonNull ContextConsumer consumer) { - CommandSource source = subject.getCommandSource().orElse(null); - if (source == null) { - return; - } - - if (source instanceof Locatable) { - World world = ((Locatable) source).getWorld(); - if (this.dimensionType) { - consumer.accept(DefaultContextKeys.DIMENSION_TYPE_KEY, getCatalogTypeName(world.getDimension().getType())); - } - if (this.world) { - this.plugin.getConfiguration().get(ConfigKeys.WORLD_REWRITES).rewriteAndSubmit(world.getName(), consumer); + if (subject instanceof Locatable) { + World world = ((Locatable) subject).world(); + consumer.accept(DefaultContextKeys.DIMENSION_TYPE_KEY, getContextKey(world.worldType().key(RegistryTypes.WORLD_TYPE))); + if (world instanceof ServerWorld) { + this.plugin.getConfiguration().get(ConfigKeys.WORLD_REWRITES).rewriteAndSubmit(getContextKey(((ServerWorld) world).key()), consumer); } } - if (this.gamemode && source instanceof ValueContainer) { - ValueContainer valueContainer = (ValueContainer) source; - valueContainer.get(Keys.GAME_MODE).ifPresent(mode -> consumer.accept(DefaultContextKeys.GAMEMODE_KEY, getCatalogTypeName(mode))); + if (subject instanceof ValueContainer) { + ValueContainer valueContainer = (ValueContainer) subject; + valueContainer.get(Keys.GAME_MODE).ifPresent(mode -> consumer.accept(DefaultContextKeys.GAMEMODE_KEY, getContextKey(mode.key(RegistryTypes.GAME_MODE)))); } } @Override - public @NonNull ContextSet estimatePotentialContexts() { + public ContextSet estimatePotentialContexts() { ImmutableContextSet.Builder builder = new ImmutableContextSetImpl.BuilderImpl(); Game game = this.plugin.getBootstrap().getGame(); - if (this.gamemode) { - for (GameMode mode : game.getRegistry().getAllOf(CatalogTypes.GAME_MODE)) { - builder.add(DefaultContextKeys.GAMEMODE_KEY, getCatalogTypeName(mode)); - } - } - if (this.dimensionType) { - for (DimensionType dim : game.getRegistry().getAllOf(CatalogTypes.DIMENSION_TYPE)) { - builder.add(DefaultContextKeys.DIMENSION_TYPE_KEY, getCatalogTypeName(dim)); - } - } - if (this.world && game.isServerAvailable()) { - for (World world : game.getServer().getWorlds()) { - String worldName = world.getName(); + game.registry(RegistryTypes.GAME_MODE).stream().forEach(mode -> { + builder.add(DefaultContextKeys.GAMEMODE_KEY, getContextKey(mode.key(RegistryTypes.GAME_MODE))); + }); + + if (game.isServerAvailable()) { + Server server = game.server(); + + server.registry(RegistryTypes.WORLD_TYPE).stream().forEach(dim -> { + builder.add(DefaultContextKeys.DIMENSION_TYPE_KEY, getContextKey(dim.key(RegistryTypes.WORLD_TYPE))); + }); + + for (ServerWorld world : server.worldManager().worlds()) { + String worldName = getContextKey(world.key()); if (Context.isValidValue(worldName)) { builder.add(DefaultContextKeys.WORLD_KEY, worldName); } @@ -121,37 +100,29 @@ public void calculate(@NonNull Subject subject, @NonNull ContextConsumer consume return builder.build(); } - private static String getCatalogTypeName(CatalogType type) { - String id = type.getId(); - if (id.startsWith("minecraft:")){ - return id.substring("minecraft:".length()); + private static String getContextKey(ResourceKey key) { + if (key.namespace().equals("minecraft")) { + return key.value(); } - return id; + return key.formatted(); } @Listener(order = Order.LAST) - public void onWorldChange(MoveEntityEvent.Teleport e) { - if (!(this.world || this.dimensionType)) { - return; - } - - Entity targetEntity = e.getTargetEntity(); + public void onWorldChange(ChangeEntityWorldEvent.Post e) { + Entity targetEntity = e.entity(); if (!(targetEntity instanceof Subject)) { return; } - if (e.getFromTransform().getExtent().equals(e.getToTransform().getExtent())) { - return; - } - this.plugin.getContextManager().signalContextUpdate((Subject) targetEntity); } - @Listener(order = Order.LAST) - public void onGameModeChange(ChangeGameModeEvent e) { - Humanoid targetEntity = e.getTargetEntity(); - if (this.gamemode && targetEntity instanceof Subject) { - this.plugin.getContextManager().signalContextUpdate((Subject) targetEntity); - } - } + // TODO: find replacement + //@Listener(order = Order.LAST) + //public void onGameModeChange(ChangeGameModeEvent e) { + // Humanoid targetEntity = e.getHumanoid(); + // if (targetEntity instanceof Subject) { + // this.plugin.getContextManager().signalContextUpdate((Subject) targetEntity); + // } + //} } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongeCommandListUpdater.java b/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongeCommandListUpdater.java new file mode 100644 index 000000000..74cc41b98 --- /dev/null +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongeCommandListUpdater.java @@ -0,0 +1,65 @@ +/* + * 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.sponge.listeners; + +import me.lucko.luckperms.common.event.listeners.AbstractCommandListUpdater; +import me.lucko.luckperms.sponge.LPSpongePlugin; +import org.spongepowered.api.command.manager.CommandManager; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; + +import java.util.UUID; + +/** + * Calls {@link CommandManager#updateCommandTreeForPlayer(ServerPlayer)} + * when a players permissions change. + */ +public class SpongeCommandListUpdater extends AbstractCommandListUpdater { + public SpongeCommandListUpdater(LPSpongePlugin plugin) { + super(plugin, ServerPlayer.class); + } + + @Override + protected boolean isServerAvailable() { + return true; + } + + @Override + protected UUID getUniqueId(ServerPlayer player) { + return player.uniqueId(); + } + + @Override + protected void sendCommandListUpdate(UUID uniqueId) { + this.plugin.getBootstrap().getScheduler().executeSync(() -> { + ServerPlayer player = this.plugin.getBootstrap().getPlayer(uniqueId).orElse(null); + if (player != null) { + CommandManager commandManager = this.plugin.getBootstrap().getGame().server().commandManager(); + commandManager.updateCommandTreeForPlayer(player); + } + }); + } + +} diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongeConnectionListener.java b/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongeConnectionListener.java index ac6e9fbe6..38fc86d22 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongeConnectionListener.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongeConnectionListener.java @@ -31,23 +31,21 @@ import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; import me.lucko.luckperms.sponge.LPSpongePlugin; -import me.lucko.luckperms.sponge.SpongeSenderFactory; - -import net.kyori.adventure.text.Component; - import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.Order; import org.spongepowered.api.event.filter.IsCancelled; -import org.spongepowered.api.event.network.ClientConnectionEvent; +import org.spongepowered.api.event.network.ServerSideConnectionEvent; import org.spongepowered.api.profile.GameProfile; import org.spongepowered.api.util.Tristate; import java.util.Collections; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.UUID; public class SpongeConnectionListener extends AbstractConnectionListener { + private final LPSpongePlugin plugin; private final Set deniedAsyncLogin = Collections.synchronizedSet(new HashSet<>()); @@ -60,22 +58,22 @@ public SpongeConnectionListener(LPSpongePlugin plugin) { @Listener(order = Order.EARLY) @IsCancelled(Tristate.UNDEFINED) - public void onClientAuth(ClientConnectionEvent.Auth e) { + public void onClientAuth(ServerSideConnectionEvent.Auth e) { /* Called when the player first attempts a connection with the server. Listening on AFTER_PRE priority to allow plugins to modify username / UUID data here. (auth plugins) Also, give other plugins a chance to cancel the event. */ - final GameProfile profile = e.getProfile(); - final String username = profile.getName().orElseThrow(() -> new RuntimeException("No username present for user " + profile.getUniqueId())); + final GameProfile profile = e.profile(); + final String username = profile.name().orElseThrow(() -> new RuntimeException("No username present for user " + profile.uniqueId())); if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { - this.plugin.getLogger().info("Processing auth event for " + profile.getUniqueId() + " - " + profile.getName()); + this.plugin.getLogger().info("Processing auth event for " + profile.uniqueId() + " - " + profile.name()); } if (e.isCancelled()) { // another plugin has disallowed the login. - this.plugin.getLogger().info("Another plugin has cancelled the connection for " + profile.getUniqueId() + " - " + username + ". No permissions data will be loaded."); - this.deniedAsyncLogin.add(profile.getUniqueId()); + this.plugin.getLogger().info("Another plugin has cancelled the connection for " + profile.uniqueId() + " - " + username + ". No permissions data will be loaded."); + this.deniedAsyncLogin.add(profile.uniqueId()); return; } @@ -89,34 +87,32 @@ public void onClientAuth(ClientConnectionEvent.Auth e) { - creating a user instance in the UserManager for this connection. - setting up cached data. */ try { - User user = loadUser(profile.getUniqueId(), username); - recordConnection(profile.getUniqueId()); - this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(profile.getUniqueId(), username, user); + User user = loadUser(profile.uniqueId(), username); + recordConnection(profile.uniqueId()); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(profile.uniqueId(), username, user); } catch (Exception ex) { - this.plugin.getLogger().severe("Exception occurred whilst loading data for " + profile.getUniqueId() + " - " + profile.getName(), ex); + this.plugin.getLogger().severe("Exception occurred whilst loading data for " + profile.uniqueId() + " - " + profile.name(), ex); - this.deniedAsyncLogin.add(profile.getUniqueId()); + this.deniedAsyncLogin.add(profile.uniqueId()); e.setCancelled(true); - e.setMessageCancelled(false); - Component reason = TranslationManager.render(Message.LOADING_DATABASE_ERROR.build()); - e.setMessage(SpongeSenderFactory.toNativeText(reason)); - this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(profile.getUniqueId(), username, null); + e.setMessage(TranslationManager.render(Message.LOADING_DATABASE_ERROR.build())); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(profile.uniqueId(), username, null); } } @Listener(order = Order.LAST) @IsCancelled(Tristate.UNDEFINED) - public void onClientAuthMonitor(ClientConnectionEvent.Auth e) { + public void onClientAuthMonitor(ServerSideConnectionEvent.Auth e) { /* Listen to see if the event was cancelled after we initially handled the connection If the connection was cancelled here, we need to do something to clean up the data that was loaded. */ // Check to see if this connection was denied at LOW. - if (this.deniedAsyncLogin.remove(e.getProfile().getUniqueId())) { + if (this.deniedAsyncLogin.remove(e.profile().uniqueId())) { // This is a problem, as they were denied at low priority, but are now being allowed. if (e.isCancelled()) { - this.plugin.getLogger().severe("Player connection was re-allowed for " + e.getProfile().getUniqueId()); + this.plugin.getLogger().severe("Player connection was re-allowed for " + e.profile().uniqueId()); e.setCancelled(true); } } @@ -124,59 +120,58 @@ public void onClientAuthMonitor(ClientConnectionEvent.Auth e) { @Listener(order = Order.FIRST) @IsCancelled(Tristate.UNDEFINED) - public void onClientLogin(ClientConnectionEvent.Login e) { + public void onClientLogin(ServerSideConnectionEvent.Login e) { /* Called when the player starts logging into the server. At this point, the users data should be present and loaded. Listening on LOW priority to allow plugins to further modify data here. (auth plugins, etc.) */ - final GameProfile profile = e.getProfile(); + final GameProfile profile = e.profile(); if (this.plugin.getConfiguration().get(ConfigKeys.DEBUG_LOGINS)) { - this.plugin.getLogger().info("Processing login event for " + profile.getUniqueId() + " - " + profile.getName()); + this.plugin.getLogger().info("Processing login event for " + profile.uniqueId() + " - " + profile.name()); } - final User user = this.plugin.getUserManager().getIfLoaded(profile.getUniqueId()); + final User user = this.plugin.getUserManager().getIfLoaded(profile.uniqueId()); /* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */ if (user == null) { - this.deniedLogin.add(profile.getUniqueId()); + this.deniedLogin.add(profile.uniqueId()); - if (!getUniqueConnections().contains(profile.getUniqueId())) { - this.plugin.getLogger().warn("User " + profile.getUniqueId() + " - " + profile.getName() + + if (!getUniqueConnections().contains(profile.uniqueId())) { + this.plugin.getLogger().warn("User " + profile.uniqueId() + " - " + profile.name() + " doesn't have data pre-loaded, they have never been processed during pre-login in this session." + " - denying login."); } else { - this.plugin.getLogger().warn("User " + profile.getUniqueId() + " - " + profile.getName() + + this.plugin.getLogger().warn("User " + profile.uniqueId() + " - " + profile.name() + " doesn't currently have data pre-loaded, but they have been processed before in this session." + " - denying login."); } e.setCancelled(true); - e.setMessageCancelled(false); - Component reason = TranslationManager.render(Message.LOADING_STATE_ERROR.build()); - e.setMessage(SpongeSenderFactory.toNativeText(reason)); + e.setMessage(TranslationManager.render(Message.LOADING_STATE_ERROR.build())); } } @Listener(order = Order.LAST) @IsCancelled(Tristate.UNDEFINED) - public void onClientLoginMonitor(ClientConnectionEvent.Login e) { + public void onClientLoginMonitor(ServerSideConnectionEvent.Login e) { /* Listen to see if the event was cancelled after we initially handled the login If the connection was cancelled here, we need to do something to clean up the data that was loaded. */ // Check to see if this connection was denied at LOW. Even if it was denied at LOW, their data will still be present. - if (this.deniedLogin.remove(e.getProfile().getUniqueId())) { + if (this.deniedLogin.remove(e.profile().uniqueId())) { // This is a problem, as they were denied at low priority, but are now being allowed. if (!e.isCancelled()) { - this.plugin.getLogger().severe("Player connection was re-allowed for " + e.getProfile().getUniqueId()); + this.plugin.getLogger().severe("Player connection was re-allowed for " + e.profile().uniqueId()); e.setCancelled(true); } } } @Listener(order = Order.POST) - public void onClientLeave(ClientConnectionEvent.Disconnect e) { - handleDisconnect(e.getTargetEntity().getUniqueId()); + public void onClientLeave(ServerSideConnectionEvent.Disconnect e) { + Optional profile = e.profile(); + profile.ifPresent(p -> handleDisconnect(p.uniqueId())); } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongePlatformListener.java b/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongePlatformListener.java index 6aacad42c..feb8f60cc 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongePlatformListener.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/listeners/SpongePlatformListener.java @@ -27,10 +27,11 @@ import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.sponge.LPSpongePlugin; - -import org.spongepowered.api.command.CommandSource; +import org.spongepowered.api.command.CommandCause; import org.spongepowered.api.event.Listener; -import org.spongepowered.api.event.command.SendCommandEvent; +import org.spongepowered.api.event.command.ExecuteCommandEvent; + +import java.util.Locale; public class SpongePlatformListener { private final LPSpongePlugin plugin; @@ -40,13 +41,12 @@ public SpongePlatformListener(LPSpongePlugin plugin) { } @Listener - public void onSendCommand(SendCommandEvent e) { - CommandSource source = e.getCause().first(CommandSource.class).orElse(null); - if (source == null) return; + public void onSendCommand(ExecuteCommandEvent e) { + CommandCause source = e.commandCause(); - final String name = e.getCommand().toLowerCase(); - if ((name.equals("op") || name.equals("minecraft:op")) && source.hasPermission("minecraft.command.op") || (name.equals("deop") || name.equals("minecraft:deop")) && source.hasPermission("minecraft.command.deop")) { - Message.OP_DISABLED_SPONGE.send(this.plugin.getSenderFactory().wrap(source)); + final String name = e.command().toLowerCase(Locale.ROOT); + if (((name.equals("op") || name.equals("minecraft:op")) && source.hasPermission("minecraft.command.op")) || ((name.equals("deop") || name.equals("minecraft:deop")) && source.hasPermission("minecraft.command.deop"))) { + Message.OP_DISABLED_SPONGE.send(this.plugin.getSenderFactory().wrap(source.audience())); } } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/messaging/PluginMessageMessenger.java b/sponge/src/main/java/me/lucko/luckperms/sponge/messaging/PluginMessageMessenger.java index ca2707277..6d528084b 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/messaging/PluginMessageMessenger.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/messaging/PluginMessageMessenger.java @@ -26,20 +26,18 @@ package me.lucko.luckperms.sponge.messaging; import com.google.common.collect.Iterables; - +import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; import me.lucko.luckperms.sponge.LPSpongePlugin; - import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; -import net.luckperms.api.messenger.message.OutgoingMessage; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.Platform; -import org.spongepowered.api.entity.living.player.Player; -import org.spongepowered.api.network.ChannelBinding; -import org.spongepowered.api.network.ChannelBuf; -import org.spongepowered.api.network.RawDataListener; -import org.spongepowered.api.network.RemoteConnection; +import org.spongepowered.api.ResourceKey; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.network.ServerConnectionState; +import org.spongepowered.api.network.channel.ChannelBuf; +import org.spongepowered.api.network.channel.raw.RawDataChannel; +import org.spongepowered.api.network.channel.raw.play.RawPlayDataHandler; +import org.spongepowered.api.scheduler.ScheduledTask; +import org.spongepowered.api.scheduler.Task; import java.util.Collection; import java.util.concurrent.TimeUnit; @@ -47,52 +45,64 @@ /** * An implementation of {@link Messenger} using the plugin messaging channels. */ -public class PluginMessageMessenger implements Messenger, RawDataListener { - private static final String CHANNEL = "luckperms:update"; +public class PluginMessageMessenger extends AbstractPluginMessageMessenger implements RawPlayDataHandler { + private static final ResourceKey CHANNEL = ResourceKey.resolve(AbstractPluginMessageMessenger.CHANNEL); private final LPSpongePlugin plugin; - private final IncomingMessageConsumer consumer; - private ChannelBinding.RawDataChannel channel = null; + private RawDataChannel channel = null; public PluginMessageMessenger(LPSpongePlugin plugin, IncomingMessageConsumer consumer) { + super(consumer); this.plugin = plugin; - this.consumer = consumer; } public void init() { - this.channel = this.plugin.getBootstrap().getGame().getChannelRegistrar().createRawChannel(this.plugin.getBootstrap(), CHANNEL); - this.channel.addListener(Platform.Type.SERVER, this); + this.channel = this.plugin.getBootstrap().getGame().channelManager().ofType(CHANNEL, RawDataChannel.class); + this.channel.play().addHandler(ServerConnectionState.Game.class, this); } @Override public void close() { if (this.channel != null) { - this.plugin.getBootstrap().getGame().getChannelRegistrar().unbindChannel(this.channel); + this.channel.play().removeHandler(this); } } @Override - public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { - this.plugin.getBootstrap().getSpongeScheduler().createTaskBuilder().interval(10, TimeUnit.SECONDS).execute(task -> { - if (!this.plugin.getBootstrap().getGame().isServerAvailable()) { - return; - } - - Collection players = this.plugin.getBootstrap().getGame().getServer().getOnlinePlayers(); - Player p = Iterables.getFirst(players, null); - if (p == null) { - return; - } - - this.channel.sendTo(p, buf -> buf.writeUTF(outgoingMessage.asEncodedString())); - task.cancel(); - }).submit(this.plugin.getBootstrap()); + protected void sendOutgoingMessage(byte[] buf) { + if (!this.plugin.getBootstrap().getGame().isServerAvailable()) { + return; + } + + Task task = Task.builder() + .interval(10, TimeUnit.SECONDS) + .execute(t -> sendOutgoingMessage(buf, t)) + .plugin(this.plugin.getBootstrap().getPluginContainer()) + .build(); + + this.plugin.getBootstrap().getScheduler().getSyncScheduler().submit(task); + } + + private void sendOutgoingMessage(byte[] buf, ScheduledTask scheduledTask) { + if (!this.plugin.getBootstrap().getGame().isServerAvailable()) { + scheduledTask.cancel(); + return; + } + + Collection players = this.plugin.getBootstrap().getGame().server().onlinePlayers(); + ServerPlayer p = Iterables.getFirst(players, null); + if (p == null) { + return; + } + + this.channel.play().sendTo(p, channelBuf -> channelBuf.writeBytes(buf)); + scheduledTask.cancel(); } @Override - public void handlePayload(@NonNull ChannelBuf buf, @NonNull RemoteConnection connection, Platform.@NonNull Type type) { - String msg = buf.readUTF(); - this.consumer.consumeIncomingMessageAsString(msg); + public void handlePayload(ChannelBuf channelBuf, ServerConnectionState.Game state) { + byte[] buf = channelBuf.readBytes(channelBuf.available()); + handleIncomingMessage(buf); } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/messaging/SpongeMessagingFactory.java b/sponge/src/main/java/me/lucko/luckperms/sponge/messaging/SpongeMessagingFactory.java index 88b84d0a1..186936a2d 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/messaging/SpongeMessagingFactory.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/messaging/SpongeMessagingFactory.java @@ -29,11 +29,9 @@ import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; import me.lucko.luckperms.common.messaging.MessagingFactory; import me.lucko.luckperms.sponge.LPSpongePlugin; - import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; - import org.checkerframework.checker.nullness.qual.NonNull; public class SpongeMessagingFactory extends MessagingFactory { diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeGroupManager.java b/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeGroupManager.java index 161ee9ef5..a011ce14a 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeGroupManager.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeGroupManager.java @@ -31,8 +31,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; - -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.model.manager.group.AbstractGroupManager; import me.lucko.luckperms.common.node.matcher.StandardNodeMatchers; import me.lucko.luckperms.common.storage.misc.DataConstraints; @@ -45,20 +44,18 @@ import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.event.cause.CreationCause; import net.luckperms.api.node.Node; import net.luckperms.api.util.Tristate; - import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.SubjectCollection; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; @@ -132,12 +129,12 @@ public CompletableFuture loadSubject(String identifier) { throw new IllegalArgumentException("Illegal subject identifier"); } - LPSubject present = this.subjectLoadingCache.getIfPresent(identifier.toLowerCase()); + LPSubject present = this.subjectLoadingCache.getIfPresent(identifier.toLowerCase(Locale.ROOT)); if (present != null) { return CompletableFuture.completedFuture(present); } - return CompletableFuture.supplyAsync(() -> this.subjectLoadingCache.get(identifier.toLowerCase()), this.plugin.getBootstrap().getScheduler().async()); + return CompletableFuture.supplyAsync(() -> this.subjectLoadingCache.get(identifier.toLowerCase(Locale.ROOT)), this.plugin.getBootstrap().getScheduler().async()); } @Override @@ -146,7 +143,7 @@ public Optional getSubject(String identifier) { return Optional.empty(); } - return Optional.ofNullable(getIfLoaded(identifier.toLowerCase())).map(SpongeGroup::sponge); + return Optional.ofNullable(getIfLoaded(identifier.toLowerCase(Locale.ROOT))).map(SpongeGroup::sponge); } @Override @@ -155,18 +152,18 @@ public CompletableFuture hasRegistered(String identifier) { return CompletableFuture.completedFuture(false); } - return CompletableFuture.completedFuture(isLoaded(identifier.toLowerCase())); + return CompletableFuture.completedFuture(isLoaded(identifier.toLowerCase(Locale.ROOT))); } @Override - public CompletableFuture> loadSubjects(Set identifiers) { + public CompletableFuture> loadSubjects(Iterable identifiers) { return CompletableFuture.supplyAsync(() -> { ImmutableSet.Builder subjects = ImmutableSet.builder(); for (String id : identifiers) { if (!DataConstraints.GROUP_NAME_TEST.test(id)) { continue; } - subjects.add(loadSubject(id.toLowerCase()).join()); + subjects.add(loadSubject(id.toLowerCase(Locale.ROOT)).join()); } return subjects.build(); diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeUserManager.java b/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeUserManager.java index fe200f929..1219c5484 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeUserManager.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/model/manager/SpongeUserManager.java @@ -31,8 +31,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; - -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.model.manager.user.AbstractUserManager; import me.lucko.luckperms.common.model.manager.user.UserHousekeeper; import me.lucko.luckperms.common.node.matcher.StandardNodeMatchers; @@ -46,11 +45,9 @@ import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.node.Node; import net.luckperms.api.util.Tristate; - import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.SubjectCollection; @@ -58,7 +55,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -170,7 +166,7 @@ public CompletableFuture hasRegistered(String identifier) { } @Override - public CompletableFuture> loadSubjects(Set identifiers) { + public CompletableFuture> loadSubjects(Iterable identifiers) { return CompletableFuture.supplyAsync(() -> { ImmutableSet.Builder subjects = ImmutableSet.builder(); for (String id : identifiers) { diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java index f0bc30992..259bca26b 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/LuckPermsService.java @@ -27,9 +27,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; - import me.lucko.luckperms.common.cache.LoadingMap; -import me.lucko.luckperms.common.context.ContextManager; +import me.lucko.luckperms.common.context.manager.ContextManager; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.LPSpongePlugin; import me.lucko.luckperms.sponge.model.manager.SpongeGroupManager; @@ -39,21 +38,26 @@ import me.lucko.luckperms.sponge.service.model.LPPermissionService; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; +import me.lucko.luckperms.sponge.service.model.LPSubjectData; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; import me.lucko.luckperms.sponge.service.model.SimplePermissionDescription; +import me.lucko.luckperms.sponge.service.model.SubjectDataUpdateEventImpl; +import me.lucko.luckperms.sponge.service.model.TemporaryCauseHolderSubject; import me.lucko.luckperms.sponge.service.model.persisted.DefaultsCollection; import me.lucko.luckperms.sponge.service.model.persisted.PersistedCollection; import me.lucko.luckperms.sponge.service.model.persisted.SubjectStorage; import me.lucko.luckperms.sponge.service.reference.SubjectReferenceFactory; - -import org.spongepowered.api.entity.living.player.Player; -import org.spongepowered.api.plugin.PluginContainer; +import net.kyori.adventure.text.Component; +import net.luckperms.api.context.ImmutableContextSet; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.event.Cause; +import org.spongepowered.api.event.permission.SubjectDataUpdateEvent; import org.spongepowered.api.service.context.ContextCalculator; -import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.Subject; -import org.spongepowered.api.text.Text; +import org.spongepowered.plugin.PluginContainer; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -73,7 +77,7 @@ public class LuckPermsService implements LPPermissionService { /** * A cached proxy of this instance */ - private final PermissionService spongeProxy; + private final PermissionAndContextService spongeProxy; /** * Reference factory, used to obtain {@link LPSubjectReference}s. @@ -120,12 +124,12 @@ public LuckPermsService(LPSpongePlugin plugin) { // load known collections for (String identifier : this.storage.getSavedCollections()) { - if (this.collections.containsKey(identifier.toLowerCase())) { + if (this.collections.containsKey(identifier.toLowerCase(Locale.ROOT))) { continue; } // load data - PersistedCollection collection = new PersistedCollection(this, identifier.toLowerCase()); + PersistedCollection collection = new PersistedCollection(this, identifier.toLowerCase(Locale.ROOT)); collection.loadAll(); // cache in this instance @@ -134,7 +138,7 @@ public LuckPermsService(LPSpongePlugin plugin) { } @Override - public PermissionService sponge() { + public PermissionAndContextService sponge() { return this.spongeProxy; } @@ -144,7 +148,7 @@ public LPSpongePlugin getPlugin() { } @Override - public ContextManager getContextManager() { + public ContextManager getContextManager() { return this.plugin.getContextManager(); } @@ -185,7 +189,7 @@ public Predicate getIdentifierValidityPredicate() { @Override public LPSubjectCollection getCollection(String s) { Objects.requireNonNull(s); - return this.collections.get(s.toLowerCase()); + return this.collections.get(s.toLowerCase(Locale.ROOT)); } @Override @@ -194,7 +198,7 @@ public ImmutableMap getLoadedCollections() { } @Override - public LPPermissionDescription registerPermissionDescription(String id, Text description, PluginContainer owner) { + public LPPermissionDescription registerPermissionDescription(String id, Component description, PluginContainer owner) { Objects.requireNonNull(id, "id"); SimplePermissionDescription desc = new SimplePermissionDescription(this, id, description, owner); this.permissionDescriptions.put(id, desc); @@ -224,11 +228,30 @@ public ImmutableSet getDescriptions() { } @Override - public void registerContextCalculator(ContextCalculator calculator) { + public void registerContextCalculator(ContextCalculator calculator) { Objects.requireNonNull(calculator); this.plugin.getContextManager().registerCalculator(new ContextCalculatorProxy(calculator)); } + @Override + public ImmutableContextSet getContextsForCause(Cause cause) { + Objects.requireNonNull(cause, "cause"); + return this.plugin.getContextManager().getContext(new TemporaryCauseHolderSubject(cause)); + } + + @Override + public ImmutableContextSet getContextsForCurrentCause() { + return getContextsForCause(this.plugin.getBootstrap().getGame().server().causeStackManager().currentCause()); + } + + @Override + public void fireUpdateEvent(LPSubjectData subjectData) { + this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + SubjectDataUpdateEvent event = new SubjectDataUpdateEventImpl(this.plugin, subjectData); + this.plugin.getBootstrap().getGame().eventManager().post(event); + }); + } + @Override public void invalidateAllCaches() { for (LPSubjectCollection collection : this.collections.values()) { diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/ProxyFactory.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/ProxyFactory.java index f72b2aa4b..58116ba05 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/ProxyFactory.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/ProxyFactory.java @@ -27,16 +27,17 @@ import me.lucko.luckperms.sponge.service.model.LPPermissionDescription; import me.lucko.luckperms.sponge.service.model.LPPermissionService; +import me.lucko.luckperms.sponge.service.model.LPProxiedSubject; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; import me.lucko.luckperms.sponge.service.model.LPSubjectData; -import me.lucko.luckperms.sponge.service.model.ProxiedSubject; - +import me.lucko.luckperms.sponge.service.proxy.PermissionDescriptionProxy; +import me.lucko.luckperms.sponge.service.proxy.PermissionServiceProxy; +import me.lucko.luckperms.sponge.service.proxy.SubjectCollectionProxy; +import me.lucko.luckperms.sponge.service.proxy.SubjectDataProxy; +import me.lucko.luckperms.sponge.service.proxy.SubjectProxy; import net.luckperms.api.model.data.DataType; - import org.spongepowered.api.service.permission.PermissionDescription; -import org.spongepowered.api.service.permission.PermissionService; -import org.spongepowered.api.service.permission.Subject; import org.spongepowered.api.service.permission.SubjectCollection; import org.spongepowered.api.service.permission.SubjectData; @@ -46,51 +47,25 @@ public final class ProxyFactory { private ProxyFactory() {} - private static final boolean IS_API_7 = isApi7(); - private static boolean isApi7() { - try { - Subject.class.getDeclaredMethod("asSubjectReference"); - return true; - } catch (NoSuchMethodException e) { - return false; - } - } - - public static PermissionService toSponge(LPPermissionService luckPerms) { - return IS_API_7 ? - new me.lucko.luckperms.sponge.service.proxy.api7.PermissionServiceProxy(luckPerms) : - new me.lucko.luckperms.sponge.service.proxy.api6.PermissionServiceProxy(luckPerms); + public static PermissionAndContextService toSponge(LPPermissionService luckPerms) { + return new PermissionServiceProxy(luckPerms); } public static SubjectCollection toSponge(LPSubjectCollection luckPerms) { - return IS_API_7 ? - new me.lucko.luckperms.sponge.service.proxy.api7.SubjectCollectionProxy(luckPerms) : - new me.lucko.luckperms.sponge.service.proxy.api6.SubjectCollectionProxy(luckPerms.getService(), luckPerms); + return new SubjectCollectionProxy(luckPerms); } - public static ProxiedSubject toSponge(LPSubject luckPerms) { - return IS_API_7 ? - new me.lucko.luckperms.sponge.service.proxy.api7.SubjectProxy(luckPerms.getService(), luckPerms.toReference()) : - new me.lucko.luckperms.sponge.service.proxy.api6.SubjectProxy(luckPerms.getService(), luckPerms.toReference()); + public static LPProxiedSubject toSponge(LPSubject luckPerms) { + return new SubjectProxy(luckPerms.getService(), luckPerms.toReference()); } public static SubjectData toSponge(LPSubjectData luckPerms) { LPSubject parentSubject = luckPerms.getParentSubject(); - return IS_API_7 ? - new me.lucko.luckperms.sponge.service.proxy.api7.SubjectDataProxy(parentSubject.getService(), parentSubject.toReference(), luckPerms.getType() == DataType.NORMAL) : - new me.lucko.luckperms.sponge.service.proxy.api6.SubjectDataProxy(parentSubject.getService(), parentSubject.toReference(), luckPerms.getType() == DataType.NORMAL); + return new SubjectDataProxy(parentSubject.getService(), parentSubject.toReference(), luckPerms.getType() == DataType.NORMAL); } public static PermissionDescription toSponge(LPPermissionDescription luckPerms) { - return IS_API_7 ? - new me.lucko.luckperms.sponge.service.proxy.api7.PermissionDescriptionProxy(luckPerms.getService(), luckPerms) : - new me.lucko.luckperms.sponge.service.proxy.api6.PermissionDescriptionProxy(luckPerms.getService(), luckPerms); - } - - public static LPPermissionDescription registerDescription(LPPermissionService service, PermissionDescription description) { - return IS_API_7 ? - me.lucko.luckperms.sponge.service.proxy.api7.DescriptionBuilder.registerDescription(service, description) : - me.lucko.luckperms.sponge.service.proxy.api6.DescriptionBuilder.registerDescription(service, description); + return new PermissionDescriptionProxy(luckPerms.getService(), luckPerms); } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/inheritance/SubjectInheritanceGraph.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/inheritance/SubjectInheritanceGraph.java index ca419d4e4..7430ffb70 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/inheritance/SubjectInheritanceGraph.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/inheritance/SubjectInheritanceGraph.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.common.graph.Graph; import me.lucko.luckperms.sponge.service.model.calculated.CalculatedSubject; - import net.luckperms.api.query.QueryOptions; import java.util.stream.Collectors; diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/ContextCalculatorProxy.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/ContextCalculatorProxy.java index cb43a76f2..1c5b74bfa 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/ContextCalculatorProxy.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/ContextCalculatorProxy.java @@ -25,29 +25,40 @@ package me.lucko.luckperms.sponge.service.model; -import me.lucko.luckperms.common.context.ForwardingContextCalculator; - +import me.lucko.luckperms.common.context.calculator.ForwardingContextCalculator; import net.luckperms.api.context.ContextConsumer; - import org.checkerframework.checker.nullness.qual.NonNull; +import org.spongepowered.api.event.Cause; +import org.spongepowered.api.event.EventContext; +import org.spongepowered.api.event.EventContextKeys; import org.spongepowered.api.service.context.Context; import org.spongepowered.api.service.context.ContextCalculator; import org.spongepowered.api.service.permission.Subject; -import java.util.Collection; -import java.util.Iterator; -import java.util.Set; +import java.util.function.Consumer; public class ContextCalculatorProxy implements ForwardingContextCalculator { - private final ContextCalculator delegate; + private final ContextCalculator delegate; - public ContextCalculatorProxy(ContextCalculator delegate) { + public ContextCalculatorProxy(ContextCalculator delegate) { this.delegate = delegate; } @Override public void calculate(@NonNull Subject subject, @NonNull ContextConsumer consumer) { - this.delegate.accumulateContexts(subject, new ForwardingContextSet(consumer)); + EventContext eventContext = EventContext.builder() + .add(EventContextKeys.SUBJECT, subject) + .build(); + + Cause cause = Cause.builder() + .append(subject) + .build(eventContext); + + calculate(cause, consumer); + } + + public void calculate(@NonNull Cause cause, @NonNull ContextConsumer consumer) { + this.delegate.accumulateContexts(cause, new ForwardingContextConsumer(consumer)); } @Override @@ -55,42 +66,20 @@ public Object delegate() { return this.delegate; } - private static final class ForwardingContextSet implements Set { + private static final class ForwardingContextConsumer implements Consumer { private final ContextConsumer consumer; - private ForwardingContextSet(ContextConsumer consumer) { + private ForwardingContextConsumer(ContextConsumer consumer) { this.consumer = consumer; } @Override - public boolean add(Context context) { - if (!net.luckperms.api.context.Context.isValidKey(context.getKey()) || - !net.luckperms.api.context.Context.isValidValue(context.getValue())) { - return false; + public void accept(Context context) { + if (net.luckperms.api.context.Context.isValidKey(context.getKey()) && + net.luckperms.api.context.Context.isValidValue(context.getValue())) { + this.consumer.accept(context.getKey(), context.getValue()); } - this.consumer.accept(context.getKey(), context.getValue()); - return true; } - - @Override - public boolean addAll(@NonNull Collection c) { - for (Context context : c) { - add(context); - } - return true; - } - - @Override public int size() { throw new UnsupportedOperationException(); } - @Override public boolean isEmpty() { throw new UnsupportedOperationException(); } - @Override public boolean contains(Object o) { throw new UnsupportedOperationException(); } - @Override public @NonNull Iterator iterator() { throw new UnsupportedOperationException(); } - @Override public @NonNull Object[] toArray() { throw new UnsupportedOperationException(); } - @Override public @NonNull T[] toArray(@NonNull T[] a) { throw new UnsupportedOperationException(); } - @Override public boolean remove(Object o) { throw new UnsupportedOperationException(); } - @Override public boolean containsAll(@NonNull Collection c) { throw new UnsupportedOperationException(); } - @Override public boolean retainAll(@NonNull Collection c) { throw new UnsupportedOperationException(); } - @Override public boolean removeAll(@NonNull Collection c) { throw new UnsupportedOperationException(); } - @Override public void clear() { throw new UnsupportedOperationException(); } } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/SimplePermissionDescription.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/SimplePermissionDescription.java index a297af14f..eef979ee6 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/SimplePermissionDescription.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/SimplePermissionDescription.java @@ -26,11 +26,10 @@ package me.lucko.luckperms.sponge.service.model; import me.lucko.luckperms.sponge.service.ProxyFactory; - +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; -import org.spongepowered.api.plugin.PluginContainer; import org.spongepowered.api.service.permission.PermissionDescription; -import org.spongepowered.api.text.Text; +import org.spongepowered.plugin.PluginContainer; import java.util.Map; import java.util.Objects; @@ -41,12 +40,12 @@ public final class SimplePermissionDescription implements LPPermissionDescriptio private final LPPermissionService service; private final String id; - private final @Nullable Text description; + private final @Nullable Component description; private final @Nullable PluginContainer owner; private PermissionDescription spongeProxy = null; - public SimplePermissionDescription(LPPermissionService service, String id, @Nullable Text description, @Nullable PluginContainer owner) { + public SimplePermissionDescription(LPPermissionService service, String id, @Nullable Component description, @Nullable PluginContainer owner) { this.service = service; this.id = Objects.requireNonNull(id, "id"); this.description = description; @@ -72,7 +71,7 @@ public String getId() { } @Override - public Optional getDescription() { + public Optional getDescription() { return Optional.ofNullable(this.description); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/events/LPSubjectDataUpdateEvent.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/SubjectDataUpdateEventImpl.java similarity index 81% rename from sponge/src/main/java/me/lucko/luckperms/sponge/service/events/LPSubjectDataUpdateEvent.java rename to sponge/src/main/java/me/lucko/luckperms/sponge/service/model/SubjectDataUpdateEventImpl.java index b2aaa353c..9dd44a10a 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/events/LPSubjectDataUpdateEvent.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/SubjectDataUpdateEventImpl.java @@ -23,30 +23,28 @@ * SOFTWARE. */ -package me.lucko.luckperms.sponge.service.events; +package me.lucko.luckperms.sponge.service.model; import me.lucko.luckperms.sponge.LPSpongePlugin; -import me.lucko.luckperms.sponge.service.model.LPSubjectData; - import org.checkerframework.checker.nullness.qual.NonNull; -import org.spongepowered.api.event.cause.Cause; -import org.spongepowered.api.event.cause.EventContext; -import org.spongepowered.api.event.cause.EventContextKeys; +import org.spongepowered.api.event.Cause; +import org.spongepowered.api.event.EventContext; +import org.spongepowered.api.event.EventContextKeys; import org.spongepowered.api.event.impl.AbstractEvent; import org.spongepowered.api.event.permission.SubjectDataUpdateEvent; import org.spongepowered.api.service.permission.SubjectData; -public class LPSubjectDataUpdateEvent extends AbstractEvent implements SubjectDataUpdateEvent { +public class SubjectDataUpdateEventImpl extends AbstractEvent implements SubjectDataUpdateEvent { private final LPSpongePlugin plugin; private final LPSubjectData subjectData; - public LPSubjectDataUpdateEvent(LPSpongePlugin plugin, LPSubjectData subjectData) { + public SubjectDataUpdateEventImpl(LPSpongePlugin plugin, LPSubjectData subjectData) { this.plugin = plugin; this.subjectData = subjectData; } @Override - public SubjectData getUpdatedData() { + public SubjectData updatedData() { return this.subjectData.sponge(); } @@ -55,7 +53,7 @@ public LPSubjectData getLuckPermsUpdatedData() { } @Override - public @NonNull Cause getCause() { + public @NonNull Cause cause() { EventContext eventContext = EventContext.builder() .add(EventContextKeys.PLUGIN, this.plugin.getBootstrap().getPluginContainer()) .build(); diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/TemporaryCauseHolderSubject.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/TemporaryCauseHolderSubject.java new file mode 100644 index 000000000..a84c5f91f --- /dev/null +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/TemporaryCauseHolderSubject.java @@ -0,0 +1,108 @@ +/* + * 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.sponge.service.model; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.api.event.Cause; +import org.spongepowered.api.event.EventContextKeys; +import org.spongepowered.api.service.context.Context; +import org.spongepowered.api.service.permission.Subject; +import org.spongepowered.api.service.permission.SubjectCollection; +import org.spongepowered.api.service.permission.SubjectData; +import org.spongepowered.api.service.permission.SubjectReference; +import org.spongepowered.api.util.Tristate; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class TemporaryCauseHolderSubject implements Subject { + private final @NonNull Cause cause; + private final @Nullable Subject subject; + + public TemporaryCauseHolderSubject(@NonNull Cause cause) { + this.cause = cause; + this.subject = subjectFromCause(this.cause); + } + + private static Subject subjectFromCause(Cause cause) { + Subject subject = cause.context().get(EventContextKeys.SUBJECT).orElse(null); + if (subject != null) { + return subject; + } + + return cause.first(Subject.class).orElse(null); + } + + public @NonNull Cause getCause() { + return this.cause; + } + + public @Nullable Subject getSubject() { + return this.subject; + } + + @Override + public String toString() { + return "CauseSubject(cause=" + this.cause + ')'; + } + + @Override + public boolean equals(Object o) { + return o == this || (o instanceof TemporaryCauseHolderSubject && this.cause.equals(((TemporaryCauseHolderSubject) o).cause)); + } + + @Override + public int hashCode() { + return this.cause.hashCode(); + } + + @Override public SubjectCollection containingCollection() { throw new UnsupportedOperationException(); } + @Override public SubjectReference asSubjectReference() { throw new UnsupportedOperationException(); } + @Override public Optional associatedObject() { throw new UnsupportedOperationException(); } + @Override public Cause contextCause() { throw new UnsupportedOperationException(); } + @Override public boolean isSubjectDataPersisted() { throw new UnsupportedOperationException(); } + @Override public SubjectData subjectData() { throw new UnsupportedOperationException(); } + @Override public SubjectData transientSubjectData() { throw new UnsupportedOperationException(); } + @Override public boolean hasPermission(String permission) { throw new UnsupportedOperationException(); } + @Override public boolean hasPermission(String permission, Cause cause) { throw new UnsupportedOperationException(); } + @Override public boolean hasPermission(String permission, Set contexts) { throw new UnsupportedOperationException(); } + @Override public Tristate permissionValue(String permission) { throw new UnsupportedOperationException(); } + @Override public Tristate permissionValue(String permission, Cause cause) { throw new UnsupportedOperationException(); } + @Override public Tristate permissionValue(String permission, Set contexts) { throw new UnsupportedOperationException(); } + @Override public boolean isChildOf(SubjectReference parent) { throw new UnsupportedOperationException(); } + @Override public boolean isChildOf(SubjectReference parent, Cause cause) { throw new UnsupportedOperationException(); } + @Override public boolean isChildOf(SubjectReference parent, Set contexts) { throw new UnsupportedOperationException(); } + @Override public List parents() { throw new UnsupportedOperationException(); } + @Override public List parents(Cause cause) { throw new UnsupportedOperationException(); } + @Override public List parents(Set contexts) { throw new UnsupportedOperationException(); } + @Override public Optional option(String key) { throw new UnsupportedOperationException(); } + @Override public Optional option(String key, Cause cause) { throw new UnsupportedOperationException(); } + @Override public Optional option(String key, Set contexts) { throw new UnsupportedOperationException(); } + @Override public String identifier() { throw new UnsupportedOperationException(); } + @Override public Optional friendlyIdentifier() { throw new UnsupportedOperationException(); } +} diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubject.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubject.java index e8870b80d..a5148a295 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubject.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubject.java @@ -26,18 +26,17 @@ package me.lucko.luckperms.sponge.service.model.calculated; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator; import me.lucko.luckperms.common.graph.TraversalAlgorithm; import me.lucko.luckperms.common.query.QueryOptionsImpl; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import me.lucko.luckperms.sponge.LPSpongePlugin; import me.lucko.luckperms.sponge.service.inheritance.SubjectInheritanceGraph; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import net.luckperms.api.context.ImmutableContextSet; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.types.MetaNode; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; @@ -67,9 +66,9 @@ public LPSubject getDefaults() { @Override public abstract CalculatedSubjectData getTransientSubjectData(); - public Map getCombinedPermissions(QueryOptions filter) { - Map permissions; - Map merging; + public Map getCombinedPermissions(QueryOptions filter) { + Map permissions; + Map merging; switch (getParentCollection().getResolutionOrder()) { case TRANSIENT_FIRST: permissions = getTransientSubjectData().resolvePermissions(filter); @@ -83,18 +82,18 @@ public Map getCombinedPermissions(QueryOptions filter) { throw new AssertionError(); } - for (Map.Entry entry : merging.entrySet()) { + for (Map.Entry entry : merging.entrySet()) { permissions.putIfAbsent(entry.getKey(), entry.getValue()); } return permissions; } - public void resolveAllPermissions(Map accumulator, QueryOptions filter) { + public void resolveAllPermissions(Map accumulator, QueryOptions filter) { SubjectInheritanceGraph graph = new SubjectInheritanceGraph(filter); Iterable traversal = graph.traverse(TraversalAlgorithm.DEPTH_FIRST_PRE_ORDER, this); for (CalculatedSubject subject : traversal) { - for (Map.Entry entry : subject.getCombinedPermissions(filter).entrySet()) { + for (Map.Entry entry : subject.getCombinedPermissions(filter).entrySet()) { accumulator.putIfAbsent(entry.getKey(), entry.getValue()); } } @@ -132,9 +131,9 @@ public Set resolveAllParents(QueryOptions filter) { return result; } - public Map getCombinedOptions(QueryOptions filter) { - Map options; - Map merging; + public Map getCombinedOptions(QueryOptions filter) { + Map options; + Map merging; switch (getParentCollection().getResolutionOrder()) { case TRANSIENT_FIRST: options = getTransientSubjectData().resolveOptions(filter); @@ -148,7 +147,7 @@ public Map getCombinedOptions(QueryOptions filter) { throw new AssertionError(); } - for (Map.Entry entry : merging.entrySet()) { + for (Map.Entry entry : merging.entrySet()) { options.putIfAbsent(entry.getKey(), entry.getValue()); } return options; @@ -160,8 +159,8 @@ public Map resolveAllOptions(QueryOptions filter) { Iterable traversal = graph.traverse(TraversalAlgorithm.DEPTH_FIRST_PRE_ORDER, this); for (CalculatedSubject subject : traversal) { - for (Map.Entry entry : subject.getCombinedOptions(filter).entrySet()) { - result.putIfAbsent(entry.getKey(), entry.getValue()); + for (MetaNode entry : subject.getCombinedOptions(filter).values()) { + result.putIfAbsent(entry.getMetaKey(), entry.getMetaValue()); } } @@ -170,18 +169,20 @@ public Map resolveAllOptions(QueryOptions filter) { public void resolveAllOptions(MetaAccumulator accumulator, QueryOptions filter) { SubjectInheritanceGraph graph = new SubjectInheritanceGraph(filter); + Iterable traversal = graph.traverse(TraversalAlgorithm.DEPTH_FIRST_PRE_ORDER, this); for (CalculatedSubject subject : traversal) { - for (Map.Entry entry : subject.getCombinedOptions(filter).entrySet()) { - accumulator.accumulateMeta(entry.getKey(), entry.getValue()); + for (MetaNode entry : subject.getCombinedOptions(filter).values()) { + accumulator.accumulateNode(entry); } } + accumulator.complete(); } @Override public Tristate getPermissionValue(QueryOptions options, String permission) { - return this.cachedData.getPermissionData(options).checkPermission(permission, PermissionCheckEvent.Origin.INTERNAL).result(); + return this.cachedData.getPermissionData(options).checkPermission(permission, CheckOrigin.INTERNAL).result(); } @Override @@ -201,7 +202,7 @@ public ImmutableList getParents(ImmutableContextSet contexts @Override public Optional getOption(ImmutableContextSet contexts, String key) { - return Optional.ofNullable(this.cachedData.getMetaData(QueryOptionsImpl.DEFAULT_CONTEXTUAL.toBuilder().context(contexts).build()).getMetaValue(key, MetaCheckEvent.Origin.PLATFORM_API)); + return Optional.ofNullable(this.cachedData.getMetaData(QueryOptionsImpl.DEFAULT_CONTEXTUAL.toBuilder().context(contexts).build()).getMetaValue(key, CheckOrigin.PLATFORM_API).result()); } @Override diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubjectCachedDataManager.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubjectCachedDataManager.java index eb03ca20a..9e9e3c287 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubjectCachedDataManager.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubjectCachedDataManager.java @@ -26,25 +26,26 @@ package me.lucko.luckperms.sponge.service.model.calculated; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.cacheddata.AbstractCachedDataManager; import me.lucko.luckperms.common.cacheddata.CacheMetadata; +import me.lucko.luckperms.common.cacheddata.metastack.SimpleMetaStackDefinition; +import me.lucko.luckperms.common.cacheddata.metastack.StandardStackElements; import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorMonitored; import me.lucko.luckperms.common.calculator.processor.DirectProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; -import me.lucko.luckperms.common.metastacking.SimpleMetaStackDefinition; -import me.lucko.luckperms.common.metastacking.StandardStackElements; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; -import me.lucko.luckperms.sponge.calculator.FixedDefaultsProcessor; - +import me.lucko.luckperms.sponge.calculator.FixedTypeDefaultsProcessor; +import me.lucko.luckperms.sponge.calculator.RootDefaultsProcessor; import net.luckperms.api.metastacking.DuplicateRemovalFunction; import net.luckperms.api.metastacking.MetaStackDefinition; import net.luckperms.api.node.ChatMetaType; +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; import java.util.ArrayList; @@ -68,7 +69,7 @@ public class CalculatedSubjectCachedDataManager extends AbstractCachedDataManage @Override protected CacheMetadata getMetadataForQueryOptions(QueryOptions queryOptions) { - VerboseCheckTarget target = VerboseCheckTarget.of(this.subject.getParentCollection().getIdentifier(), this.subject.getIdentifier()); + VerboseCheckTarget target = VerboseCheckTarget.of(this.subject.getParentCollection().getIdentifier(), this.subject.getIdentifier().getName()); return new CacheMetadata(null, target, queryOptions); } @@ -88,7 +89,7 @@ protected MetaStackDefinition getDefaultMetaStackDefinition(ChatMetaType type) { } @Override - protected > M resolvePermissions(IntFunction mapFactory, QueryOptions queryOptions) { + protected > M resolvePermissions(IntFunction mapFactory, QueryOptions queryOptions) { M map = mapFactory.apply(16); this.subject.resolveAllPermissions(map, queryOptions); return map; @@ -100,16 +101,17 @@ protected void resolveMeta(MetaAccumulator accumulator, QueryOptions queryOption } @Override - public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { - List processors = new ArrayList<>(4); - processors.add(new DirectProcessor()); - processors.add(new SpongeWildcardProcessor()); - processors.add(new WildcardProcessor()); + public PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata) { + List processors = new ArrayList<>(5); + processors.add(new DirectProcessor(sourceMap)); + processors.add(new SpongeWildcardProcessor(sourceMap)); + processors.add(new WildcardProcessor(sourceMap)); if (!this.subject.getParentCollection().isDefaultsCollection()) { - processors.add(new FixedDefaultsProcessor(this.subject.getService(), queryOptions, this.subject.getDefaults(), true)); + processors.add(new FixedTypeDefaultsProcessor(this.subject.getService(), queryOptions, this.subject.getDefaults(), true)); + processors.add(new RootDefaultsProcessor(this.subject.getService(), queryOptions, true)); } - return new PermissionCalculator(getPlugin(), metadata, processors); + return new PermissionCalculatorMonitored(getPlugin(), metadata, processors); } } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubjectData.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubjectData.java index f98843789..c15f77e51 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubjectData.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/CalculatedSubjectData.java @@ -28,26 +28,31 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; - import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.ContextSetComparator; +import me.lucko.luckperms.common.context.comparator.ContextSetComparator; +import me.lucko.luckperms.common.model.InheritanceOrigin; +import me.lucko.luckperms.common.node.types.Meta; +import me.lucko.luckperms.common.node.types.Permission; import me.lucko.luckperms.sponge.service.ProxyFactory; import me.lucko.luckperms.sponge.service.model.LPPermissionService; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectData; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import net.luckperms.api.context.ContextSatisfyMode; import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.Node; +import net.luckperms.api.node.metadata.types.InheritanceOriginMetadata; +import net.luckperms.api.node.types.MetaNode; +import net.luckperms.api.node.types.PermissionNode; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; - import org.spongepowered.api.service.permission.SubjectData; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -62,6 +67,7 @@ public class CalculatedSubjectData implements LPSubjectData { private final LPSubject parentSubject; private final DataType type; + private final InheritanceOrigin inheritanceOrigin; private final LPPermissionService service; private final Map> permissions = new ConcurrentHashMap<>(); @@ -71,6 +77,7 @@ public class CalculatedSubjectData implements LPSubjectData { public CalculatedSubjectData(LPSubject parentSubject, DataType type, LPPermissionService service) { this.parentSubject = parentSubject; this.type = type; + this.inheritanceOrigin = new InheritanceOrigin(this.parentSubject.getIdentifier(), this.type); this.service = service; } @@ -133,21 +140,32 @@ public ImmutableMap getPermissions(ImmutableContextSet contexts return ImmutableMap.copyOf(this.permissions.getOrDefault(contexts, ImmutableMap.of())); } - public Map resolvePermissions(QueryOptions filter) { + public Map resolvePermissions(QueryOptions filter) { // get relevant entries - SortedMap> sorted = new TreeMap<>(ContextSetComparator.reverse()); + SortedMap> sorted = new TreeMap<>(ContextSetComparator.descending()); for (Map.Entry> entry : this.permissions.entrySet()) { if (!filter.satisfies(entry.getKey(), defaultSatisfyMode())) { continue; } - sorted.put(entry.getKey(), entry.getValue()); + Map nodeMap = new HashMap<>(); + entry.getValue().forEach((key, value) -> { + PermissionNode node = Permission.builder() + .permission(key) + .value(value) + .context(entry.getKey()) + .withMetadata(InheritanceOriginMetadata.KEY, this.inheritanceOrigin) + .build(); + nodeMap.put(key, node); + }); + + sorted.put(entry.getKey(), nodeMap); } // flatten - Map result = new HashMap<>(); - for (Map map : sorted.values()) { - for (Map.Entry e : map.entrySet()) { + Map result = new HashMap<>(); + for (Map map : sorted.values()) { + for (Map.Entry e : map.entrySet()) { result.putIfAbsent(e.getKey(), e.getValue()); } } @@ -160,10 +178,10 @@ public CompletableFuture setPermission(ImmutableContextSet contexts, St boolean b; if (value == Tristate.UNDEFINED) { Map perms = this.permissions.get(contexts); - b = perms != null && perms.remove(permission.toLowerCase()) != null; + b = perms != null && perms.remove(permission.toLowerCase(Locale.ROOT)) != null; } else { Map perms = this.permissions.computeIfAbsent(contexts, c -> new ConcurrentHashMap<>()); - b = !Objects.equals(perms.put(permission.toLowerCase(), value.asBoolean()), value.asBoolean()); + b = !Objects.equals(perms.put(permission.toLowerCase(Locale.ROOT), value.asBoolean()), value.asBoolean()); } if (b) { this.service.invalidateAllCaches(); @@ -213,7 +231,7 @@ public ImmutableList getParents(ImmutableContextSet contexts public Set resolveParents(QueryOptions filter) { // get relevant entries - SortedMap> sorted = new TreeMap<>(ContextSetComparator.reverse()); + SortedMap> sorted = new TreeMap<>(ContextSetComparator.descending()); for (Map.Entry> entry : this.parents.entrySet()) { if (!filter.satisfies(entry.getKey(), defaultSatisfyMode())) { continue; @@ -287,21 +305,32 @@ public ImmutableMap getOptions(ImmutableContextSet contexts) { return ImmutableMap.copyOf(this.options.getOrDefault(contexts, ImmutableMap.of())); } - public Map resolveOptions(QueryOptions filter) { + public Map resolveOptions(QueryOptions filter) { // get relevant entries - SortedMap> sorted = new TreeMap<>(ContextSetComparator.reverse()); + SortedMap> sorted = new TreeMap<>(ContextSetComparator.descending()); for (Map.Entry> entry : this.options.entrySet()) { if (!filter.satisfies(entry.getKey(), defaultSatisfyMode())) { continue; } - sorted.put(entry.getKey(), entry.getValue()); + Map nodeMap = new HashMap<>(); + entry.getValue().forEach((key, value) -> { + MetaNode node = Meta.builder() + .key(key) + .value(value) + .context(entry.getKey()) + .withMetadata(InheritanceOriginMetadata.KEY, this.inheritanceOrigin) + .build(); + nodeMap.put(key, node); + }); + + sorted.put(entry.getKey(), nodeMap); } // flatten - Map result = new HashMap<>(); - for (Map map : sorted.values()) { - for (Map.Entry e : map.entrySet()) { + Map result = new HashMap<>(); + for (Map map : sorted.values()) { + for (Map.Entry e : map.entrySet()) { result.putIfAbsent(e.getKey(), e.getValue()); } } @@ -312,7 +341,7 @@ public Map resolveOptions(QueryOptions filter) { @Override public CompletableFuture setOption(ImmutableContextSet contexts, String key, String value) { Map options = this.options.computeIfAbsent(contexts, c -> new ConcurrentHashMap<>()); - boolean b = !stringEquals(options.put(key.toLowerCase(), value), value); + boolean b = !stringEquals(options.put(key.toLowerCase(Locale.ROOT), value), value); if (b) { this.service.invalidateAllCaches(); } @@ -322,7 +351,7 @@ public CompletableFuture setOption(ImmutableContextSet contexts, String @Override public CompletableFuture unsetOption(ImmutableContextSet contexts, String key) { Map options = this.options.get(contexts); - boolean b = options != null && options.remove(key.toLowerCase()) != null; + boolean b = options != null && options.remove(key.toLowerCase(Locale.ROOT)) != null; if (b) { this.service.invalidateAllCaches(); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/MonitoredSubjectData.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/MonitoredSubjectData.java index 408c5da58..800cee826 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/MonitoredSubjectData.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/calculated/MonitoredSubjectData.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.sponge.service.LuckPermsService; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.util.Tristate; diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/GroupSubject.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/GroupSubject.java index e70d68dbc..d738042bd 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/GroupSubject.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/GroupSubject.java @@ -25,13 +25,12 @@ package me.lucko.luckperms.sponge.service.model.permissionholder; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; import me.lucko.luckperms.sponge.LPSpongePlugin; import me.lucko.luckperms.sponge.model.SpongeGroup; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; -import org.spongepowered.api.command.CommandSource; - import java.util.Optional; /** @@ -43,8 +42,8 @@ public GroupSubject(LPSpongePlugin plugin, SpongeGroup parent) { } @Override - public String getIdentifier() { - return this.parent.getObjectName(); + public PermissionHolderIdentifier getIdentifier() { + return this.parent.getIdentifier(); } @Override @@ -52,11 +51,6 @@ public Optional getFriendlyIdentifier() { return this.parent.getDisplayName(); } - @Override - public Optional getCommandSource() { - return Optional.empty(); - } - @Override public LPSubjectCollection getParentCollection() { return this.plugin.getService().getGroupSubjects(); diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/PermissionHolderSubject.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/PermissionHolderSubject.java index 341147e9f..b98f33b74 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/PermissionHolderSubject.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/PermissionHolderSubject.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.sponge.service.model.permissionholder; import com.google.common.collect.ImmutableList; - import me.lucko.luckperms.common.cacheddata.type.MetaCache; import me.lucko.luckperms.common.graph.TraversalAlgorithm; import me.lucko.luckperms.common.inheritance.InheritanceGraph; @@ -35,22 +34,18 @@ import me.lucko.luckperms.common.node.types.Inheritance; import me.lucko.luckperms.common.node.types.Prefix; import me.lucko.luckperms.common.node.types.Suffix; -import me.lucko.luckperms.common.verbose.event.MetaCheckEvent; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import me.lucko.luckperms.sponge.LPSpongePlugin; import me.lucko.luckperms.sponge.model.SpongeGroup; import me.lucko.luckperms.sponge.service.LuckPermsService; import me.lucko.luckperms.sponge.service.ProxyFactory; -import me.lucko.luckperms.sponge.service.events.UpdateEventHandler; +import me.lucko.luckperms.sponge.service.model.LPProxiedSubject; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; -import me.lucko.luckperms.sponge.service.model.ProxiedSubject; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.query.QueryOptions; import net.luckperms.api.util.Tristate; - import org.spongepowered.api.service.permission.PermissionService; import java.util.Optional; @@ -65,7 +60,7 @@ public abstract class PermissionHolderSubject implem private final PermissionHolderSubjectData subjectData; private final PermissionHolderSubjectData transientSubjectData; - private ProxiedSubject spongeSubject = null; + private LPProxiedSubject spongeSubject = null; PermissionHolderSubject(LPSpongePlugin plugin, T parent) { this.parent = parent; @@ -75,8 +70,8 @@ public abstract class PermissionHolderSubject implem } public void fireUpdateEvent() { - UpdateEventHandler.fireUpdateEvent(this.plugin, this.subjectData); - UpdateEventHandler.fireUpdateEvent(this.plugin, this.transientSubjectData); + this.plugin.getService().fireUpdateEvent(this.subjectData); + this.plugin.getService().fireUpdateEvent(this.transientSubjectData); } public T getParent() { @@ -84,7 +79,7 @@ public T getParent() { } @Override - public synchronized ProxiedSubject sponge() { + public synchronized LPProxiedSubject sponge() { if (this.spongeSubject == null) { this.spongeSubject = ProxyFactory.toSponge(this); } @@ -113,7 +108,7 @@ public PermissionHolderSubjectData getTransientSubjectData() { @Override public Tristate getPermissionValue(QueryOptions options, String permission) { - return this.parent.getCachedData().getPermissionData(options).checkPermission(permission, PermissionCheckEvent.Origin.PLATFORM_LOOKUP_CHECK).result(); + return this.parent.getCachedData().getPermissionData(options).checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET).result(); } @Override @@ -124,8 +119,8 @@ public Tristate getPermissionValue(ImmutableContextSet contexts, String permissi @Override public boolean isChildOf(ImmutableContextSet contexts, LPSubjectReference parent) { - return parent.getCollectionIdentifier().equals(PermissionService.SUBJECTS_GROUP) && - getPermissionValue(contexts, Inheritance.key(parent.getSubjectIdentifier())).asBoolean(); + return parent.collectionIdentifier().equals(PermissionService.SUBJECTS_GROUP) && + getPermissionValue(contexts, Inheritance.key(parent.subjectIdentifier())).asBoolean(); } @Override @@ -148,20 +143,20 @@ public ImmutableList getParents(ImmutableContextSet contexts public Optional getOption(ImmutableContextSet contexts, String s) { MetaCache data = this.parent.getCachedData().getMetaData(this.plugin.getContextManager().formQueryOptions(contexts)); if (s.equalsIgnoreCase(Prefix.NODE_KEY)) { - String prefix = data.getPrefix(MetaCheckEvent.Origin.PLATFORM_API); + String prefix = data.getPrefix(CheckOrigin.PLATFORM_API).result(); if (prefix != null) { return Optional.of(prefix); } } if (s.equalsIgnoreCase(Suffix.NODE_KEY)) { - String suffix = data.getSuffix(MetaCheckEvent.Origin.PLATFORM_API); + String suffix = data.getSuffix(CheckOrigin.PLATFORM_API).result(); if (suffix != null) { return Optional.of(suffix); } } - String val = data.getMetaValue(s, MetaCheckEvent.Origin.PLATFORM_API); + String val = data.getMetaValue(s, CheckOrigin.PLATFORM_API).result(); if (val != null) { return Optional.of(val); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/PermissionHolderSubjectData.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/PermissionHolderSubjectData.java index b510ef1d4..f490f5dda 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/PermissionHolderSubjectData.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/PermissionHolderSubjectData.java @@ -27,7 +27,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; - import me.lucko.luckperms.common.cacheddata.type.MetaAccumulator; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.HolderType; @@ -44,7 +43,6 @@ import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectData; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.model.data.DataType; import net.luckperms.api.node.ChatMetaType; @@ -55,12 +53,12 @@ import net.luckperms.api.node.types.PrefixNode; import net.luckperms.api.node.types.SuffixNode; import net.luckperms.api.util.Tristate; - import org.spongepowered.api.service.permission.PermissionService; import org.spongepowered.api.service.permission.SubjectData; import java.util.Collection; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -183,11 +181,11 @@ public CompletableFuture addParent(ImmutableContextSet contexts, LPSubj Objects.requireNonNull(contexts, "contexts"); Objects.requireNonNull(subject, "subject"); - if (!subject.getCollectionIdentifier().equals(PermissionService.SUBJECTS_GROUP)) { + if (!subject.collectionIdentifier().equals(PermissionService.SUBJECTS_GROUP)) { return CompletableFuture.completedFuture(false); } - Node node = Inheritance.builder(subject.getSubjectIdentifier()) + Node node = Inheritance.builder(subject.subjectIdentifier()) .withContext(contexts) .build(); @@ -203,11 +201,11 @@ public CompletableFuture removeParent(ImmutableContextSet contexts, LPS Objects.requireNonNull(contexts, "contexts"); Objects.requireNonNull(subject, "subject"); - if (!subject.getCollectionIdentifier().equals(PermissionService.SUBJECTS_GROUP)) { + if (!subject.collectionIdentifier().equals(PermissionService.SUBJECTS_GROUP)) { return CompletableFuture.completedFuture(false); } - Node node = Inheritance.builder(subject.getSubjectIdentifier()) + Node node = Inheritance.builder(subject.subjectIdentifier()) .withContext(contexts) .build(); @@ -297,7 +295,7 @@ public CompletableFuture setOption(ImmutableContextSet contexts, String Node node; if (key.equalsIgnoreCase(Prefix.NODE_KEY) || key.equalsIgnoreCase(Suffix.NODE_KEY)) { // special handling. - ChatMetaType type = ChatMetaType.valueOf(key.toUpperCase()); + ChatMetaType type = ChatMetaType.valueOf(key.toUpperCase(Locale.ROOT)); // remove all prefixes/suffixes from the user this.holder.removeIf(this.type, contexts, type.nodeType()::matches, false); diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/UserSubject.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/UserSubject.java index 8648e0e12..95afabba9 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/UserSubject.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/permissionholder/UserSubject.java @@ -25,29 +25,28 @@ package me.lucko.luckperms.sponge.service.model.permissionholder; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; import me.lucko.luckperms.sponge.LPSpongePlugin; import me.lucko.luckperms.sponge.model.SpongeUser; import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; - +import me.lucko.luckperms.sponge.service.model.LPSubjectUser; import org.spongepowered.api.Sponge; -import org.spongepowered.api.command.CommandSource; +import org.spongepowered.api.entity.living.player.server.ServerPlayer; import java.util.Optional; -import java.util.UUID; -import java.util.function.Function; /** * Implements {@link LPSubject} for a {@link SpongeUser}. */ -public final class UserSubject extends PermissionHolderSubject implements LPSubject { +public final class UserSubject extends PermissionHolderSubject implements LPSubject, LPSubjectUser { public UserSubject(LPSpongePlugin plugin, SpongeUser parent) { super(plugin, parent); } @Override - public String getIdentifier() { - return this.parent.getUniqueId().toString(); + public PermissionHolderIdentifier getIdentifier() { + return this.parent.getIdentifier(); } @Override @@ -56,9 +55,12 @@ public Optional getFriendlyIdentifier() { } @Override - public Optional getCommandSource() { - final UUID uuid = this.parent.getUniqueId(); - return Sponge.getServer().getPlayer(uuid).map(Function.identity()); + public Optional resolvePlayer() { + if (!Sponge.isServerAvailable()) { + return Optional.empty(); + } + + return Sponge.server().player(this.parent.getUniqueId()); } @Override diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedCollection.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedCollection.java index c5e7a0df3..05b1ef6d6 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedCollection.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedCollection.java @@ -29,9 +29,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; - import me.lucko.luckperms.common.cache.LoadingMap; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.common.util.ImmutableCollectors; import me.lucko.luckperms.common.util.Predicates; import me.lucko.luckperms.sponge.service.LuckPermsService; @@ -39,16 +38,14 @@ import me.lucko.luckperms.sponge.service.model.LPSubject; import me.lucko.luckperms.sponge.service.model.LPSubjectCollection; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; - import net.luckperms.api.context.ImmutableContextSet; import net.luckperms.api.util.Tristate; - import org.spongepowered.api.service.permission.SubjectCollection; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; @@ -88,7 +85,7 @@ public PersistedCollection(LuckPermsService service, String identifier) { public void loadAll() { Map holders = this.service.getStorage().loadAllFromFile(this.identifier); for (Map.Entry e : holders.entrySet()) { - PersistedSubject subject = this.subjects.get(e.getKey().toLowerCase()); + PersistedSubject subject = this.subjects.get(e.getKey().toLowerCase(Locale.ROOT)); if (subject != null) { subject.loadData(e.getValue()); } @@ -121,7 +118,7 @@ public boolean isDefaultsCollection() { } public LPSubject obtainSubject(String identifier) { - return this.subjects.get(identifier.toLowerCase()); + return this.subjects.get(identifier.toLowerCase(Locale.ROOT)); } @Override @@ -138,14 +135,14 @@ public Optional getSubject(String identifier) { @Override public CompletableFuture hasRegistered(String identifier) { - return CompletableFuture.completedFuture(this.subjects.containsKey(identifier.toLowerCase())); + return CompletableFuture.completedFuture(this.subjects.containsKey(identifier.toLowerCase(Locale.ROOT))); } @Override - public CompletableFuture> loadSubjects(Set identifiers) { + public CompletableFuture> loadSubjects(Iterable identifiers) { ImmutableSet.Builder subjects = ImmutableSet.builder(); for (String id : identifiers) { - subjects.add(Objects.requireNonNull(this.subjects.get(id.toLowerCase()))); + subjects.add(Objects.requireNonNull(this.subjects.get(id.toLowerCase(Locale.ROOT)))); } return CompletableFuture.completedFuture(subjects.build()); } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedSubject.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedSubject.java index 2c6a05c0e..612e0665b 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedSubject.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedSubject.java @@ -26,23 +26,18 @@ package me.lucko.luckperms.sponge.service.model.persisted; import me.lucko.luckperms.common.cache.BufferedRequest; +import me.lucko.luckperms.common.model.PermissionHolderIdentifier; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.sponge.service.LuckPermsService; import me.lucko.luckperms.sponge.service.ProxyFactory; -import me.lucko.luckperms.sponge.service.events.UpdateEventHandler; +import me.lucko.luckperms.sponge.service.model.LPProxiedSubject; import me.lucko.luckperms.sponge.service.model.LPSubject; -import me.lucko.luckperms.sponge.service.model.LPSubjectData; -import me.lucko.luckperms.sponge.service.model.ProxiedSubject; import me.lucko.luckperms.sponge.service.model.calculated.CalculatedSubject; import me.lucko.luckperms.sponge.service.model.calculated.CalculatedSubjectData; import me.lucko.luckperms.sponge.service.model.calculated.MonitoredSubjectData; - import net.luckperms.api.model.data.DataType; -import org.spongepowered.api.command.CommandSource; - import java.io.IOException; -import java.util.Optional; import java.util.concurrent.TimeUnit; /** @@ -54,7 +49,7 @@ public class PersistedSubject extends CalculatedSubject implements LPSubject { /** * The subjects identifier */ - private final String identifier; + private final PermissionHolderIdentifier identifier; /** * The parent collection @@ -65,7 +60,7 @@ public class PersistedSubject extends CalculatedSubject implements LPSubject { private final PersistedSubjectData subjectData; private final CalculatedSubjectData transientSubjectData; - private ProxiedSubject spongeSubject = null; + private LPProxiedSubject spongeSubject = null; /** * The save buffer instance for saving changes to disk @@ -81,14 +76,14 @@ public PersistedSubject(LuckPermsService service, PersistedCollection parentColl super(service.getPlugin()); this.service = service; this.parentCollection = parentCollection; - this.identifier = identifier; + this.identifier = new PermissionHolderIdentifier(parentCollection.getIdentifier(), identifier); this.subjectData = new PersistedSubjectData(this, DataType.NORMAL, service) { @Override protected void onUpdate(boolean success) { super.onUpdate(success); if (success) { - fireUpdateEvent(this); + PersistedSubject.this.service.fireUpdateEvent(this); } } }; @@ -96,7 +91,7 @@ protected void onUpdate(boolean success) { @Override protected void onUpdate(boolean success) { if (success) { - fireUpdateEvent(this); + PersistedSubject.this.service.fireUpdateEvent(this); } } }; @@ -104,15 +99,6 @@ protected void onUpdate(boolean success) { this.saveBuffer = new SaveBuffer(service.getPlugin()); } - /** - * Calls the subject data update event for the given {@link LPSubjectData} instance. - * - * @param subjectData the subject data - */ - private void fireUpdateEvent(LPSubjectData subjectData) { - UpdateEventHandler.fireUpdateEvent(this.service.getPlugin(), subjectData); - } - /** * Loads data into this {@link PersistedSubject} from the given * {@link SubjectDataContainer} container @@ -148,7 +134,7 @@ void doSave() { } @Override - public ProxiedSubject sponge() { + public LPProxiedSubject sponge() { if (this.spongeSubject == null) { this.spongeSubject = ProxyFactory.toSponge(this); } @@ -156,7 +142,7 @@ public ProxiedSubject sponge() { } @Override - public String getIdentifier() { + public PermissionHolderIdentifier getIdentifier() { return this.identifier; } @@ -180,11 +166,6 @@ public CalculatedSubjectData getTransientSubjectData() { return this.transientSubjectData; } - @Override - public Optional getCommandSource() { - return Optional.empty(); - } - private final class SaveBuffer extends BufferedRequest { public SaveBuffer(LuckPermsPlugin plugin) { super(1, TimeUnit.SECONDS, plugin.getBootstrap().getScheduler()); diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedSubjectData.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedSubjectData.java index 39d94eeff..18d418bb5 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedSubjectData.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/PersistedSubjectData.java @@ -27,7 +27,6 @@ import me.lucko.luckperms.sponge.service.LuckPermsService; import me.lucko.luckperms.sponge.service.model.calculated.MonitoredSubjectData; - import net.luckperms.api.model.data.DataType; /** diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/SubjectDataContainer.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/SubjectDataContainer.java index a3efda34e..360ef4975 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/SubjectDataContainer.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/SubjectDataContainer.java @@ -31,14 +31,12 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; - -import me.lucko.luckperms.common.context.ContextSetComparator; -import me.lucko.luckperms.common.context.ContextSetJsonSerializer; +import me.lucko.luckperms.common.context.comparator.ContextSetComparator; +import me.lucko.luckperms.common.context.serializer.ContextSetJsonSerializer; import me.lucko.luckperms.sponge.service.model.LPPermissionService; import me.lucko.luckperms.sponge.service.model.LPSubjectData; import me.lucko.luckperms.sponge.service.model.LPSubjectReference; import me.lucko.luckperms.sponge.service.model.calculated.CalculatedSubjectData; - import net.luckperms.api.context.ImmutableContextSet; import java.util.ArrayList; @@ -256,8 +254,8 @@ public JsonObject serialize() { JsonArray data = new JsonArray(); for (LPSubjectReference ref : e.getValue()) { JsonObject parent = new JsonObject(); - parent.addProperty("collection", ref.getCollectionIdentifier()); - parent.addProperty("subject", ref.getSubjectIdentifier()); + parent.addProperty("collection", ref.collectionIdentifier()); + parent.addProperty("subject", ref.subjectIdentifier()); data.add(parent); } section.add("data", data); @@ -283,7 +281,7 @@ public void applyToData(CalculatedSubjectData subjectData) { private static List> sortContextMap(Map map) { List> entries = new ArrayList<>(map.entrySet()); - entries.sort((o1, o2) -> ContextSetComparator.reverse().compare(o1.getKey(), o2.getKey())); + entries.sort((o1, o2) -> ContextSetComparator.descending().compare(o1.getKey(), o2.getKey())); return entries; } diff --git a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/SubjectStorage.java b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/SubjectStorage.java index 6eb811c37..866905504 100644 --- a/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/SubjectStorage.java +++ b/sponge/src/main/java/me/lucko/luckperms/sponge/service/model/persisted/SubjectStorage.java @@ -27,7 +27,6 @@ import com.google.common.collect.ImmutableSet; import com.google.gson.JsonObject; - import me.lucko.luckperms.common.util.ImmutableCollectors; import me.lucko.luckperms.common.util.MoreFiles; import me.lucko.luckperms.common.util.gson.GsonProvider; @@ -110,7 +109,7 @@ private Path resolveFile(String collectionIdentifier, String subjectIdentifier) * @throws IOException if the write fails */ public void saveToFile(PersistedSubject subject) throws IOException { - Path subjectFile = resolveFile(subject.getParentCollection().getIdentifier(), subject.getIdentifier()); + Path subjectFile = resolveFile(subject.getParentCollection().getIdentifier(), subject.getIdentifier().getName()); saveToFile(SubjectDataContainer.copyOf(subject.getSubjectData()), subjectFile); } diff --git a/sponge/src/main/resources/luckperms.conf b/sponge/src/main/resources/luckperms.conf index 26c596ba6..fd109dc30 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 | # @@ -156,15 +156,25 @@ data { } # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). # - Change this if you want to use different tables for different servers. table-prefix = "luckperms_" - # The prefix to use for all LuckPerms collections. Change this if you want to use different - # collections for different servers. The default is no prefix. + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. mongodb-collection-prefix = "" - # MongoDB ClientConnectionURI for use with replica sets and custom connection options - # - See https://docs.mongodb.com/manual/reference/connection-string/ + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ mongodb-connection-uri = "" } @@ -234,6 +244,9 @@ watch-files = true # below. # => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be # configured below. +# => nats Uses Nats pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. # => auto Attempts to automatically setup a messaging service using redis or sql. messaging-service = "auto" @@ -252,10 +265,32 @@ 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" + username = "" + password = "" + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel { + enabled = false + master = "mymaster" + addresses = ["localhost:26379"] + username = "" + password = "" + } +} + +# Settings for nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats { + enabled = false + address = "localhost" + username = "" password = "" + token = "" } # Settings for RabbitMQ. @@ -536,6 +571,13 @@ apply-sponge-default-subjects=true # | Extra settings | # # +----------------------------------------------------------------------------------------------+ # +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators = [] + # Allows you to set "aliases" for the worlds sent forward for context calculation. # # - These aliases are provided in addition to the real world name. Applied recursively. @@ -589,11 +631,52 @@ allow-invalid-usernames = false # - If automation is needed, users should prefer using the LuckPerms API. skip-bulkupdate-confirmation = false +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate = false + # If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. # # - When this happens, the plugin will set their primary group back to default. prevent-primary-group-removal = false +# If LuckPerms should update the list of commands sent to the client when permissions are changed. +update-client-command-list = true + # If LuckPerms should attempt to resolve Vanilla command target selectors for LP commands. -# See here for more info: https://minecraft.gamepedia.com/Commands#Target_selectors +# See here for more info: https://minecraft.wiki/w/Target_selectors resolve-command-selectors = false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode { + players = false + console = false +} + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands { + players = false + console = false +} diff --git a/standalone/app/build.gradle b/standalone/app/build.gradle new file mode 100644 index 000000000..e4cc4c59f --- /dev/null +++ b/standalone/app/build.gradle @@ -0,0 +1,51 @@ +plugins { + id("java-library") +} + +tasks.withType(JavaCompile).configureEach { + options.release = 17 +} + +dependencies { + implementation project(':api') + + api 'org.apache.logging.log4j:log4j-core:2.17.2' + api 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.2' + api 'net.minecrell:terminalconsoleappender:1.3.0' + api 'org.jline:jline-terminal-jansi:3.20.0' + + api 'com.google.code.gson:gson:2.13.1' + api 'com.google.guava:guava:33.4.8-jre' + api 'io.netty:netty-all:4.2.1.Final' + + api('net.kyori:adventure-api:4.21.0') { + exclude(module: 'adventure-bom') + exclude(module: 'checker-qual') + exclude(module: 'annotations') + } + api('net.kyori:adventure-text-serializer-gson:4.21.0') { + exclude(module: 'adventure-bom') + exclude(module: 'adventure-api') + exclude(module: 'gson') + } + api('net.kyori:adventure-text-serializer-legacy:4.21.0') { + exclude(module: 'adventure-bom') + exclude(module: 'adventure-api') + } + api('net.kyori:adventure-text-serializer-plain:4.21.0') { + exclude(module: 'adventure-bom') + exclude(module: 'adventure-api') + } + api('net.kyori:adventure-text-minimessage:4.21.0') { + exclude(module: 'adventure-bom') + exclude(module: 'adventure-api') + } + api('net.kyori:adventure-text-serializer-ansi:4.21.0') { + exclude(module: 'adventure-bom') + exclude(module: 'adventure-api') + exclude(module: 'annotations') + } + api('net.kyori:ansi:1.1.1') { + exclude(module: 'annotations') + } +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java new file mode 100644 index 000000000..be1f9b078 --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/LuckPermsApplication.java @@ -0,0 +1,131 @@ +/* + * 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; + +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import me.lucko.luckperms.standalone.app.integration.ShutdownCallback; +import me.lucko.luckperms.standalone.app.utils.DockerCommandSocket; +import me.lucko.luckperms.standalone.app.utils.HeartbeatHttpServer; +import me.lucko.luckperms.standalone.app.utils.TerminalInterface; +import net.luckperms.api.LuckPerms; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * The LuckPerms standalone application. + */ +public class LuckPermsApplication implements AutoCloseable { + + /** A logger instance */ + public static final Logger LOGGER = LogManager.getLogger(LuckPermsApplication.class); + + /** A callback to shutdown the application via the loader bootstrap. */ + private final ShutdownCallback shutdownCallback; + + /** The instance of the LuckPerms API available within the app */ + private LuckPerms luckPermsApi; + /** A command executor interface to run LuckPerms commands */ + private CommandExecutor commandExecutor; + + /** If the application is running */ + private final AtomicBoolean running = new AtomicBoolean(true); + + /** The docker command socket */ + private DockerCommandSocket dockerCommandSocket; + /** The heartbeat http server */ + private HeartbeatHttpServer heartbeatHttpServer; + + public LuckPermsApplication(ShutdownCallback shutdownCallback) { + this.shutdownCallback = shutdownCallback; + } + + /** + * Start the app + */ + public void start(String[] args) { + TerminalInterface terminal = new TerminalInterface(this, this.commandExecutor); + + List arguments = Arrays.asList(args); + if (arguments.contains("--docker")) { + this.dockerCommandSocket = DockerCommandSocket.createAndStart("/opt/luckperms/luckperms.sock", terminal); + this.heartbeatHttpServer = HeartbeatHttpServer.createAndStart(3001, () -> this.luckPermsApi.runHealthCheck()); + } + + terminal.start(); // blocking + } + + public void requestShutdown() { + this.shutdownCallback.shutdown(); + } + + @Override + public void close() { + this.running.set(false); + + if (this.dockerCommandSocket != null) { + try { + this.dockerCommandSocket.close(); + } catch (Exception e) { + LOGGER.warn(e); + } + } + + if (this.heartbeatHttpServer != null) { + try { + this.heartbeatHttpServer.close(); + } catch (Exception e) { + LOGGER.warn(e); + } + } + } + + public AtomicBoolean runningState() { + return this.running; + } + + // called before start() + public void setApi(LuckPerms luckPermsApi) { + this.luckPermsApi = luckPermsApi; + } + + // called before start() + public void setCommandExecutor(CommandExecutor commandExecutor) { + this.commandExecutor = commandExecutor; + } + + public LuckPerms getApi() { + return this.luckPermsApi; + } + + public CommandExecutor getCommandExecutor() { + return this.commandExecutor; + } + +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/CommandExecutor.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/CommandExecutor.java new file mode 100644 index 000000000..c5d92a1e5 --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/CommandExecutor.java @@ -0,0 +1,48 @@ +/* + * 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.integration; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Minimal command executor interface. + */ +public interface CommandExecutor { + + CompletableFuture execute(StandaloneSender player, String command); + + List tabComplete(StandaloneSender player, String command); + + default CompletableFuture execute(String command) { + return execute(StandaloneUser.INSTANCE, command); + } + + default List tabComplete(String command) { + return tabComplete(StandaloneUser.INSTANCE, command); + } + +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/ShutdownCallback.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/ShutdownCallback.java new file mode 100644 index 000000000..1fcbd0212 --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/ShutdownCallback.java @@ -0,0 +1,37 @@ +/* + * 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.integration; + +/** + * Shutdown callback for the whole standalone app. + * + * (in practice this is always implemented by the StandaloneLoader class) + */ +public interface ShutdownCallback { + + void shutdown(); + +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneSender.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneSender.java new file mode 100644 index 000000000..eea1ad817 --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneSender.java @@ -0,0 +1,48 @@ +/* + * 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.integration; + +import net.kyori.adventure.text.Component; +import net.luckperms.api.util.Tristate; + +import java.util.Locale; +import java.util.UUID; + +public interface StandaloneSender { + String getName(); + + UUID getUniqueId(); + + void sendMessage(Component component); + + Tristate getPermissionValue(String permission); + + boolean hasPermission(String permission); + + boolean isConsole(); + + Locale getLocale(); +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneUser.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneUser.java new file mode 100644 index 000000000..4d98b009f --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/integration/StandaloneUser.java @@ -0,0 +1,84 @@ +/* + * 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.integration; + +import me.lucko.luckperms.standalone.app.LuckPermsApplication; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer; +import net.luckperms.api.util.Tristate; + +import java.util.Locale; +import java.util.UUID; + +/** + * The sender instance used for the console / users executing commands + * on a standalone instance of LuckPerms + */ +public class StandaloneUser implements StandaloneSender { + + private static final UUID UUID = new UUID(0, 0); + + public static final StandaloneUser INSTANCE = new StandaloneUser(); + + private StandaloneUser() { + } + + @Override + public String getName() { + return "StandaloneUser"; + } + + @Override + public UUID getUniqueId() { + return UUID; + } + + @Override + public void sendMessage(Component component) { + LuckPermsApplication.LOGGER.info(ANSIComponentSerializer.ansi().serialize(component)); + } + + @Override + public Tristate getPermissionValue(String permission) { + return Tristate.TRUE; + } + + @Override + public boolean hasPermission(String permission) { + return true; + } + + @Override + public boolean isConsole() { + return true; + } + + @Override + public Locale getLocale() { + return Locale.getDefault(); + } + +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/DockerCommandSocket.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/DockerCommandSocket.java new file mode 100644 index 000000000..b2435e1e4 --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/DockerCommandSocket.java @@ -0,0 +1,108 @@ +/* + * 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 org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.Channels; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Consumer; + +/** + * Simple/dumb unix domain socket that listens for connections, + * reads the input to a string, then executes it as a command. + * + *

    Combined with a small sh/nc program, this makes it easy to execute + * commands against a standalone instance of LP in a Docker container.

    + */ +public class DockerCommandSocket implements Runnable, AutoCloseable { + private static final Logger LOGGER = LogManager.getLogger(DockerCommandSocket.class); + + public static DockerCommandSocket createAndStart(String socketPath, TerminalInterface terminal) { + DockerCommandSocket socket = null; + + try { + Path path = Paths.get(socketPath); + Files.deleteIfExists(path); + + ServerSocketChannel channel = ServerSocketChannel.open(StandardProtocolFamily.UNIX); + channel.bind(UnixDomainSocketAddress.of(path)); + + socket = new DockerCommandSocket(channel, terminal::runCommand); + + Thread thread = new Thread(socket, "docker-command-socket"); + thread.setDaemon(true); + thread.start(); + } catch (Exception e) { + LOGGER.error("Error starting docker command socket", e); + } + + return socket; + } + + private final ServerSocketChannel channel; + private final Consumer callback; + + public DockerCommandSocket(ServerSocketChannel channel, Consumer callback) throws IOException { + this.channel = channel; + this.callback = callback; + } + + @Override + public void run() { + while (this.channel.isOpen()) { + try (SocketChannel socket = this.channel.accept()) { + try (BufferedReader reader = new BufferedReader(Channels.newReader(socket, StandardCharsets.UTF_8))) { + String cmd; + while ((cmd = reader.readLine()) != null) { + LOGGER.info("Executing command from Docker: " + cmd); + this.callback.accept(cmd); + } + } + } catch (ClosedChannelException e) { + // ignore + } catch (IOException e) { + LOGGER.error("Error processing input from the Docker socket", e); + } + } + } + + @Override + public void close() throws Exception { + this.channel.close(); + } +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/HeartbeatHttpServer.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/HeartbeatHttpServer.java new file mode 100644 index 000000000..262e7d78d --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/HeartbeatHttpServer.java @@ -0,0 +1,95 @@ +/* + * 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 com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import net.luckperms.api.platform.Health; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +/** + * Provides a tiny http server indicating the current status of the app + */ +public class HeartbeatHttpServer implements HttpHandler, AutoCloseable { + private static final Logger LOGGER = LogManager.getLogger(HeartbeatHttpServer.class); + + private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("heartbeat-http-server-%d") + .build() + ); + + public static HeartbeatHttpServer createAndStart(int port, Supplier healthReporter) { + HeartbeatHttpServer socket = null; + + try { + socket = new HeartbeatHttpServer(healthReporter, port); + LOGGER.info("Started healthcheck HTTP server on :" + port); + } catch (Exception e) { + LOGGER.error("Error starting Heartbeat HTTP server", e); + } + + return socket; + } + + private final Supplier healthReporter; + private final HttpServer server; + + public HeartbeatHttpServer(Supplier healthReporter, int port) throws IOException { + this.healthReporter = healthReporter; + this.server = HttpServer.create(new InetSocketAddress(port), 50); + this.server.createContext("/health", this); + this.server.setExecutor(EXECUTOR); + this.server.start(); + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + Health health = this.healthReporter.get(); + byte[] response = health.toString().getBytes(StandardCharsets.UTF_8); + + exchange.sendResponseHeaders(health.isHealthy() ? 200 : 503, response.length); + try (OutputStream responseBody = exchange.getResponseBody()) { + responseBody.write(response); + } + } + + @Override + public void close() { + this.server.stop(0); + } +} diff --git a/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/TerminalInterface.java b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/TerminalInterface.java new file mode 100644 index 000000000..856d7e1a4 --- /dev/null +++ b/standalone/app/src/main/java/me/lucko/luckperms/standalone/app/utils/TerminalInterface.java @@ -0,0 +1,98 @@ +/* + * 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 me.lucko.luckperms.standalone.app.LuckPermsApplication; +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import net.minecrell.terminalconsole.SimpleTerminalConsole; +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.ParsedLine; + +import java.util.List; + +/** + * The terminal/console-style interface presented to the user. + */ +public class TerminalInterface extends SimpleTerminalConsole { + private final LuckPermsApplication application; + private final CommandExecutor commandExecutor; + + public TerminalInterface(LuckPermsApplication application, CommandExecutor commandExecutor) { + this.application = application; + this.commandExecutor = commandExecutor; + } + + @Override + protected LineReader buildReader(LineReaderBuilder builder) { + return super.buildReader(builder + .appName("LuckPerms") + .completer(this::completeCommand) + ); + } + + @Override + protected boolean isRunning() { + return this.application.runningState().get(); + } + + @Override + protected void shutdown() { + this.application.requestShutdown(); + } + + @Override + public void runCommand(String command) { + command = stripSlashLp(command); + + if (command.equals("stop") || command.equals("exit")) { + this.application.requestShutdown(); + return; + } + + this.commandExecutor.execute(command); + } + + private void completeCommand(LineReader reader, ParsedLine line, List candidates) { + String cmdLine = stripSlashLp(line.line()); + + for (String suggestion : this.commandExecutor.tabComplete(cmdLine)) { + candidates.add(new Candidate(suggestion)); + } + } + + private static String stripSlashLp(String command) { + if (command.startsWith("/")) { + command = command.substring(1); + } + if (command.startsWith("lp ")) { + command = command.substring(3); + } + return command; + } + +} diff --git a/standalone/build.gradle b/standalone/build.gradle new file mode 100644 index 000000000..e690be90c --- /dev/null +++ b/standalone/build.gradle @@ -0,0 +1,86 @@ +plugins { + alias(libs.plugins.shadow) + id("jacoco") + id("jacoco-report-aggregation") +} + +tasks.withType(JavaCompile).configureEach { + options.release = 17 +} + +test { + useJUnitPlatform { + if (!project.hasProperty('dockerTests')) { + excludeTags 'docker' + } + } + systemProperty('net.kyori.ansi.colorLevel', 'indexed16') +} + +jacocoTestReport { + dependsOn test +} + +dependencies { + implementation project(':common') + compileOnly project(':common:loader-utils') + compileOnly project(':standalone:app') + + testImplementation 'org.junit.jupiter:junit-jupiter:5.13.0' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation 'org.testcontainers:testcontainers-junit-jupiter:2.0.2' + testImplementation 'org.mockito:mockito-core:5.18.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0' + testImplementation 'org.awaitility:awaitility:4.3.0' + + testImplementation 'com.zaxxer:HikariCP:6.3.0' + testImplementation 'redis.clients:jedis:5.2.0' + testImplementation 'io.nats:jnats:2.21.1' + testImplementation 'com.rabbitmq:amqp-client:5.25.0' + testImplementation 'org.postgresql:postgresql:42.7.6' + testImplementation 'com.h2database:h2:2.1.214' + testImplementation 'org.xerial:sqlite-jdbc:3.49.1.0' + testImplementation 'com.mysql:mysql-connector-j:9.3.0' + testImplementation 'org.mariadb.jdbc:mariadb-java-client:3.5.2' + testImplementation 'org.mongodb:mongodb-driver-legacy:5.5.0' + testImplementation 'me.lucko.configurate:configurate-toml:3.7' + testImplementation 'org.spongepowered:configurate-yaml:3.7.3' + testImplementation 'org.spongepowered:configurate-hocon:3.7.3' + testImplementation 'org.yaml:snakeyaml:1.22' + testImplementation 'net.luckperms:rest-api-java-client:0.1' + + testImplementation project(':standalone:app') + testImplementation project(':common:loader-utils') +} + +shadowJar { + archiveFileName = 'luckperms-standalone.jarinjar' + + dependencies { + include(dependency('me.lucko.luckperms:.*')) + } + + relocate 'net.kyori.event', 'me.lucko.luckperms.lib.eventbus' + relocate 'com.github.benmanes.caffeine', 'me.lucko.luckperms.lib.caffeine' + relocate 'okio', 'me.lucko.luckperms.lib.okio' + relocate 'okhttp3', 'me.lucko.luckperms.lib.okhttp3' + relocate 'net.bytebuddy', 'me.lucko.luckperms.lib.bytebuddy' + 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 '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' + relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' + relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' + relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' + relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' + relocate 'org.yaml.snakeyaml', 'me.lucko.luckperms.lib.yaml' +} + +artifacts { + archives shadowJar +} diff --git a/standalone/docker/Dockerfile b/standalone/docker/Dockerfile new file mode 100644 index 000000000..6c9ae7ddc --- /dev/null +++ b/standalone/docker/Dockerfile @@ -0,0 +1,27 @@ +FROM eclipse-temurin:21-alpine +RUN apk add --no-cache netcat-openbsd + +# create a simple 'send' command that will allow users +# to run, for example: docker exec send lp info +RUN printf '#!/bin/sh\n\ +echo "$@" | nc -NU /opt/luckperms/luckperms.sock\n' >> /usr/bin/send && chmod 777 /usr/bin/send + +# setup user +RUN addgroup -S app && adduser -S -G app app +USER app + +# copy jar file into image +WORKDIR /opt/luckperms +COPY luckperms-standalone.jar . + +# create volume for data directory +RUN mkdir data +VOLUME ["/opt/luckperms/data"] + +# preload and relocate dependency jars +RUN java -jar luckperms-standalone.jar preloadDependencies + +CMD ["java", "-jar", "luckperms-standalone.jar", "--docker"] + +HEALTHCHECK --interval=30s --timeout=15s --start-period=20s \ + CMD wget http://localhost:3001/health -q -O - | grep -c '"up":true' || exit 1 diff --git a/standalone/docker/readme.md b/standalone/docker/readme.md new file mode 100644 index 000000000..9898846ba --- /dev/null +++ b/standalone/docker/readme.md @@ -0,0 +1,6 @@ +### Docker Build instructions + +1. Compile with Gradle +2. `cd standalone/loader/build/libs` +3. `cp LuckPerms-*.jar luckperms-standalone.jar` +4. `docker build . -t luckperms -f ../../../docker/Dockerfile` diff --git a/standalone/loader/build.gradle b/standalone/loader/build.gradle new file mode 100644 index 000000000..5b549e794 --- /dev/null +++ b/standalone/loader/build.gradle @@ -0,0 +1,38 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer + +plugins { + alias(libs.plugins.shadow) + id("application") +} + +dependencies { + implementation project(':api') + implementation project(':common:loader-utils') + implementation project(':standalone:app') +} + +tasks.withType(JavaCompile).configureEach { + options.release = 17 +} + +application { + mainClass = 'me.lucko.luckperms.standalone.loader.StandaloneLoader' +} + +processResources { + include '*.xml' +} + +shadowJar { + archiveFileName = "LuckPerms-Standalone-${project.ext.fullVersion}.jar" + + from { + project(':standalone').tasks.shadowJar.archiveFile + } + + transform(Log4j2PluginsCacheFileTransformer) +} + +artifacts { + archives shadowJar +} diff --git a/standalone/loader/src/main/java/me/lucko/luckperms/standalone/loader/StandaloneLoader.java b/standalone/loader/src/main/java/me/lucko/luckperms/standalone/loader/StandaloneLoader.java new file mode 100644 index 000000000..5657e78f7 --- /dev/null +++ b/standalone/loader/src/main/java/me/lucko/luckperms/standalone/loader/StandaloneLoader.java @@ -0,0 +1,109 @@ +/* + * 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.loader; + +import me.lucko.luckperms.common.loader.JarInJarClassLoader; +import me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.standalone.app.LuckPermsApplication; +import me.lucko.luckperms.standalone.app.integration.ShutdownCallback; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; + +/** + * Loader bootstrap for LuckPerms running as a "standalone" app. + * + * There are three main modules: + * 1. the loader (this) + * - performs jar-in-jar loading for the plugin + * - starts the application + * 2. the plugin (LPStandaloneBootstrap, LPStandalonePlugin, etc) + * - implements the standard classes required to create an abstract LuckPerms "plugin") + * 3. the application + * - allows the user to interact with the plugin through a basic terminal layer + */ +public class StandaloneLoader implements ShutdownCallback { + public static final Logger LOGGER = LogManager.getLogger(StandaloneLoader.class); + + private static final String JAR_NAME = "luckperms-standalone.jarinjar"; + private static final String BOOTSTRAP_PLUGIN_CLASS = "me.lucko.luckperms.standalone.LPStandaloneBootstrap"; + private static final String BOOTSTRAP_DEPENDENCY_PRELOADER_CLASS = "me.lucko.luckperms.standalone.StandaloneDependencyPreloader"; + + private LuckPermsApplication app; + private JarInJarClassLoader loader; + private LoaderBootstrap plugin; + + // Entrypoint + public static void main(String[] args) { + Thread.setDefaultUncaughtExceptionHandler((t, e) -> LOGGER.error("Exception in thread " + t.getName(), e)); + + StandaloneLoader loader = new StandaloneLoader(); + loader.start(args); + } + + public void start(String[] args) { + // construct an application, but don't "start" it yet + this.app = new LuckPermsApplication(this); + + // create a jar-in-jar classloader for the standalone plugin, then enable it + // the application is passes to the plugin constructor, to allow it to pass hooks back + this.loader = new JarInJarClassLoader(getClass().getClassLoader(), JAR_NAME); + + // special case for dependency preload command + if (args.length == 1 && args[0].equals("preloadDependencies")) { + try { + Class clazz = this.loader.loadClass(BOOTSTRAP_DEPENDENCY_PRELOADER_CLASS); + clazz.getMethod("main").invoke(null); + } catch (Exception e) { + e.printStackTrace(); + } + return; + } + + this.plugin = this.loader.instantiatePlugin(BOOTSTRAP_PLUGIN_CLASS, LuckPermsApplication.class, this.app); + this.plugin.onLoad(); + this.plugin.onEnable(); + + // start the application + this.app.start(args); + } + + @Override + public void shutdown() { + // shutdown in reverse order + this.app.close(); + this.plugin.onDisable(); + try { + this.loader.close(); + } catch (IOException e) { + LOGGER.error(e); + } + + LogManager.shutdown(true); + } + +} diff --git a/standalone/loader/src/main/resources/log4j2.xml b/standalone/loader/src/main/resources/log4j2.xml new file mode 100644 index 000000000..ffb070356 --- /dev/null +++ b/standalone/loader/src/main/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java new file mode 100644 index 000000000..fe8cb1938 --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandaloneBootstrap.java @@ -0,0 +1,216 @@ +/* + * 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 me.lucko.luckperms.common.loader.LoaderBootstrap; +import me.lucko.luckperms.common.plugin.bootstrap.BootstrappedWithLoader; +import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.plugin.classpath.JarInJarClassPathAppender; +import me.lucko.luckperms.common.plugin.logging.Log4jPluginLogger; +import me.lucko.luckperms.common.plugin.logging.PluginLogger; +import me.lucko.luckperms.common.plugin.scheduler.JavaSchedulerAdapter; +import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; +import me.lucko.luckperms.common.util.BuildInfo; +import me.lucko.luckperms.standalone.app.LuckPermsApplication; +import net.luckperms.api.platform.Platform; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; + +/** + * Bootstrap plugin for LuckPerms running as a standalone app. + */ +public class LPStandaloneBootstrap implements LuckPermsBootstrap, LoaderBootstrap, BootstrappedWithLoader { + private final LuckPermsApplication loader; + + private final PluginLogger logger; + private final SchedulerAdapter schedulerAdapter; + private final ClassPathAppender classPathAppender; + private final LPStandalonePlugin plugin; + + private Instant startTime; + private final CountDownLatch loadLatch = new CountDownLatch(1); + private final CountDownLatch enableLatch = new CountDownLatch(1); + + public LPStandaloneBootstrap(LuckPermsApplication loader) { + this.loader = loader; + + this.logger = new Log4jPluginLogger(LuckPermsApplication.LOGGER); + this.schedulerAdapter = new JavaSchedulerAdapter(this); + this.classPathAppender = new JarInJarClassPathAppender(getClass().getClassLoader()); + this.plugin = new LPStandalonePlugin(this); + } + + // visible for testing + protected LPStandaloneBootstrap(LuckPermsApplication loader, ClassPathAppender classPathAppender) { + this.loader = loader; + + this.logger = new Log4jPluginLogger(LuckPermsApplication.LOGGER); + this.schedulerAdapter = new JavaSchedulerAdapter(this); + this.classPathAppender = classPathAppender; + this.plugin = createTestPlugin(); + } + + // visible for testing + protected LPStandalonePlugin createTestPlugin() { + return new LPStandalonePlugin(this); + } + + // provide adapters + + @Override + public LuckPermsApplication getLoader() { + return this.loader; + } + + @Override + public PluginLogger getPluginLogger() { + return this.logger; + } + + @Override + public SchedulerAdapter getScheduler() { + return this.schedulerAdapter; + } + + @Override + public ClassPathAppender getClassPathAppender() { + return this.classPathAppender; + } + + // lifecycle + + @Override + public void onLoad() { + try { + this.plugin.load(); + } finally { + this.loadLatch.countDown(); + } + } + + @Override + public void onEnable() { + this.startTime = Instant.now(); + try { + this.plugin.enable(); + } finally { + this.enableLatch.countDown(); + } + } + + @Override + public void onDisable() { + this.plugin.disable(); + } + + @Override + public CountDownLatch getEnableLatch() { + return this.enableLatch; + } + + @Override + public CountDownLatch getLoadLatch() { + return this.loadLatch; + } + + // provide information about the plugin + + @Override + public String getVersion() { + return BuildInfo.VERSION; + } + + @Override + public Instant getStartupTime() { + return this.startTime; + } + + // provide information about the platform + + @Override + public Platform.Type getType() { + return Platform.Type.STANDALONE; + } + + @Override + public String getServerBrand() { + return "standalone"; + } + + @Override + public String getServerVersion() { + return "n/a"; + } + + @Override + public Path getDataDirectory() { + return Paths.get("data").toAbsolutePath(); + } + + @Override + public Optional getPlayer(UUID uniqueId) { + return Optional.empty(); + } + + @Override + public Optional lookupUniqueId(String username) { + return Optional.empty(); + } + + @Override + public Optional lookupUsername(UUID uniqueId) { + return Optional.empty(); + } + + @Override + public int getPlayerCount() { + return 0; + } + + @Override + public Collection getPlayerList() { + return Collections.emptyList(); + } + + @Override + public Collection getOnlinePlayers() { + return Collections.emptyList(); + } + + @Override + public boolean isPlayerOnline(UUID uniqueId) { + return false; + } + +} diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java new file mode 100644 index 000000000..3970faa0b --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/LPStandalonePlugin.java @@ -0,0 +1,202 @@ +/* + * 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 me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.calculator.CalculatorFactory; +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import me.lucko.luckperms.common.dependencies.Dependency; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.common.messaging.MessagingFactory; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.model.manager.group.StandardGroupManager; +import me.lucko.luckperms.common.model.manager.track.StandardTrackManager; +import me.lucko.luckperms.common.model.manager.user.StandardUserManager; +import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; +import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.standalone.app.LuckPermsApplication; +import me.lucko.luckperms.standalone.app.integration.StandaloneUser; +import me.lucko.luckperms.standalone.stub.StandaloneContextManager; +import me.lucko.luckperms.standalone.stub.StandaloneDummyConnectionListener; +import me.lucko.luckperms.standalone.stub.StandaloneEventBus; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.query.QueryOptions; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +/** + * LuckPerms implementation for the standalone app. + */ +public class LPStandalonePlugin extends AbstractLuckPermsPlugin { + private final LPStandaloneBootstrap bootstrap; + + private StandaloneSenderFactory senderFactory; + private StandaloneDummyConnectionListener connectionListener; + private StandaloneCommandManager commandManager; + private StandardUserManager userManager; + private StandardGroupManager groupManager; + private StandardTrackManager trackManager; + private StandaloneContextManager contextManager; + + public LPStandalonePlugin(LPStandaloneBootstrap bootstrap) { + this.bootstrap = bootstrap; + } + + @Override + public LPStandaloneBootstrap getBootstrap() { + return this.bootstrap; + } + + public LuckPermsApplication getLoader() { + return this.bootstrap.getLoader(); + } + + @Override + protected void setupSenderFactory() { + this.senderFactory = new StandaloneSenderFactory(this); + } + + @Override + protected Set getGlobalDependencies() { + Set dependencies = super.getGlobalDependencies(); + dependencies.remove(Dependency.ADVENTURE); + dependencies.add(Dependency.CONFIGURATE_CORE); + dependencies.add(Dependency.CONFIGURATE_YAML); + dependencies.add(Dependency.SNAKEYAML); + return dependencies; + } + + @Override + protected ConfigurationAdapter provideConfigurationAdapter() { + return new StandaloneConfigAdapter(this, resolveConfig("config.yml")); + } + + @Override + protected void registerPlatformListeners() { + this.connectionListener = new StandaloneDummyConnectionListener(this); + } + + @Override + protected MessagingFactory provideMessagingFactory() { + return new StandaloneMessagingFactory(this); + } + + @Override + protected void registerCommands() { + this.commandManager = new StandaloneCommandManager(this); + this.bootstrap.getLoader().setCommandExecutor(this.commandManager); + } + + @Override + protected void setupManagers() { + this.userManager = new StandardUserManager(this); + this.groupManager = new StandardGroupManager(this); + this.trackManager = new StandardTrackManager(this); + } + + @Override + protected CalculatorFactory provideCalculatorFactory() { + return new StandaloneCalculatorFactory(this); + } + + @Override + protected void setupContextManager() { + this.contextManager = new StandaloneContextManager(this); + } + + @Override + protected void setupPlatformHooks() { + + } + + @Override + protected AbstractEventBus provideEventBus(LuckPermsApiProvider apiProvider) { + return new StandaloneEventBus(this, apiProvider); + } + + @Override + protected void registerApiOnPlatform(LuckPerms api) { + this.bootstrap.getLoader().setApi(api); + } + + @Override + protected void performFinalSetup() { + + } + + @Override + public Optional getQueryOptionsForUser(User user) { + return Optional.empty(); + } + + @Override + public Stream getOnlineSenders() { + return Stream.of(getConsoleSender()); + } + + @Override + public Sender getConsoleSender() { + return getSenderFactory().wrap(StandaloneUser.INSTANCE); + } + + public StandaloneSenderFactory getSenderFactory() { + return this.senderFactory; + } + + @Override + public AbstractConnectionListener getConnectionListener() { + return this.connectionListener; + } + + @Override + public StandaloneCommandManager getCommandManager() { + return this.commandManager; + } + + @Override + public StandardUserManager getUserManager() { + return this.userManager; + } + + @Override + public StandardGroupManager getGroupManager() { + return this.groupManager; + } + + @Override + public StandardTrackManager getTrackManager() { + return this.trackManager; + } + + @Override + public StandaloneContextManager getContextManager() { + return this.contextManager; + } + +} diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCalculatorFactory.java similarity index 69% rename from fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java rename to standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCalculatorFactory.java index 3c78fa39e..c4d500c7f 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricCalculatorFactory.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCalculatorFactory.java @@ -23,55 +23,50 @@ * SOFTWARE. */ -package me.lucko.luckperms.fabric; +package me.lucko.luckperms.standalone; import me.lucko.luckperms.common.cacheddata.CacheMetadata; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorMonitored; import me.lucko.luckperms.common.calculator.processor.DirectProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import me.lucko.luckperms.common.calculator.processor.RegexProcessor; import me.lucko.luckperms.common.calculator.processor.SpongeWildcardProcessor; import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.fabric.calculator.ServerOwnerProcessor; -import me.lucko.luckperms.fabric.context.FabricContextManager; - +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; import java.util.ArrayList; import java.util.List; +import java.util.Map; -public class FabricCalculatorFactory implements CalculatorFactory { - private final LPFabricPlugin plugin; +public class StandaloneCalculatorFactory implements CalculatorFactory { + private final LPStandalonePlugin plugin; - public FabricCalculatorFactory(LPFabricPlugin plugin) { + public StandaloneCalculatorFactory(LPStandalonePlugin plugin) { this.plugin = plugin; } @Override - public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { - List processors = new ArrayList<>(5); + public PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata) { + List processors = new ArrayList<>(8); - processors.add(new DirectProcessor()); + processors.add(new DirectProcessor(sourceMap)); if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { - processors.add(new RegexProcessor()); + processors.add(new RegexProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { - processors.add(new WildcardProcessor()); + processors.add(new WildcardProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { - processors.add(new SpongeWildcardProcessor()); - } - - boolean integratedOwner = queryOptions.option(FabricContextManager.INTEGRATED_SERVER_OWNER).orElse(false); - if (integratedOwner && this.plugin.getConfiguration().get(ConfigKeys.FABRIC_INTEGRATED_SERVER_OWNER_BYPASSES_CHECKS)) { - processors.add(new ServerOwnerProcessor()); + processors.add(new SpongeWildcardProcessor(sourceMap)); } - return new PermissionCalculator(this.plugin, metadata, processors); + return new PermissionCalculatorMonitored(this.plugin, metadata, processors); } } diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCommandManager.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCommandManager.java new file mode 100644 index 000000000..e1b67bce1 --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneCommandManager.java @@ -0,0 +1,59 @@ +/* + * 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 me.lucko.luckperms.common.command.CommandManager; +import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import me.lucko.luckperms.standalone.app.integration.StandaloneSender; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class StandaloneCommandManager extends CommandManager implements CommandExecutor { + private final LPStandalonePlugin plugin; + + public StandaloneCommandManager(LPStandalonePlugin plugin) { + super(plugin); + this.plugin = plugin; + } + + @Override + public CompletableFuture execute(StandaloneSender player, String command) { + Sender wrapped = this.plugin.getSenderFactory().wrap(player); + List arguments = ArgumentTokenizer.EXECUTE.tokenizeInput(command); + return executeCommand(wrapped, "lp", arguments); + } + + @Override + public List tabComplete(StandaloneSender player, String command) { + Sender wrapped = this.plugin.getSenderFactory().wrap(player); + List arguments = ArgumentTokenizer.TAB_COMPLETE.tokenizeInput(command); + return tabCompleteCommand(wrapped, arguments); + } + +} diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneConfigAdapter.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneConfigAdapter.java new file mode 100644 index 000000000..abb3860a4 --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneConfigAdapter.java @@ -0,0 +1,46 @@ +/* + * 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 me.lucko.luckperms.common.config.generic.adapter.ConfigurateConfigAdapter; +import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import ninja.leaping.configurate.ConfigurationNode; +import ninja.leaping.configurate.loader.ConfigurationLoader; +import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; + +import java.nio.file.Path; + +public class StandaloneConfigAdapter extends ConfigurateConfigAdapter implements ConfigurationAdapter { + public StandaloneConfigAdapter(LuckPermsPlugin plugin, Path path) { + super(plugin, path); + } + + @Override + protected ConfigurationLoader createLoader(Path path) { + return YAMLConfigurationLoader.builder().setPath(path).build(); + } +} diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneDependencyPreloader.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneDependencyPreloader.java new file mode 100644 index 000000000..0d68297c2 --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneDependencyPreloader.java @@ -0,0 +1,69 @@ +/* + * 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.util.concurrent.ThreadFactoryBuilder; +import me.lucko.luckperms.common.dependencies.Dependency; +import me.lucko.luckperms.common.dependencies.DependencyManager; +import me.lucko.luckperms.common.dependencies.DependencyManagerImpl; +import me.lucko.luckperms.common.dependencies.relocation.RelocationHandler; +import me.lucko.luckperms.common.util.MoreFiles; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Pre-loads and pre-relocates all possible dependencies. + */ +public class StandaloneDependencyPreloader { + + public static void main(String[] args) throws Exception { + main(); + } + + public static void main() throws Exception { + Path cacheDirectory = Paths.get("data").resolve("libs"); + MoreFiles.createDirectoriesIfNotExists(cacheDirectory); + + ExecutorService executorService = Executors.newFixedThreadPool(8, new ThreadFactoryBuilder().setDaemon(true).build()); + DependencyManager dependencyManager = new DependencyManagerImpl(cacheDirectory, executorService); + + Set dependencies = new HashSet<>(Arrays.asList(Dependency.values())); + System.out.println("Preloading " + dependencies.size() + " dependencies..."); + + dependencies.removeAll(RelocationHandler.DEPENDENCIES); + dependencyManager.loadDependencies(RelocationHandler.DEPENDENCIES); + dependencyManager.loadDependencies(dependencies); + + System.out.println("Done!"); + } +} + diff --git a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSchedulerAdapter.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneMessagingFactory.java similarity index 71% rename from fabric/src/main/java/me/lucko/luckperms/fabric/FabricSchedulerAdapter.java rename to standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneMessagingFactory.java index 30fc6315d..d0771f84d 100644 --- a/fabric/src/main/java/me/lucko/luckperms/fabric/FabricSchedulerAdapter.java +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneMessagingFactory.java @@ -23,21 +23,18 @@ * SOFTWARE. */ -package me.lucko.luckperms.fabric; +package me.lucko.luckperms.standalone; -import me.lucko.luckperms.common.plugin.scheduler.AbstractJavaScheduler; +import me.lucko.luckperms.common.messaging.InternalMessagingService; +import me.lucko.luckperms.common.messaging.MessagingFactory; -import java.util.concurrent.Executor; - -public class FabricSchedulerAdapter extends AbstractJavaScheduler { - private final Executor sync; - - public FabricSchedulerAdapter(LPFabricBootstrap bootstrap) { - this.sync = r -> bootstrap.getServer().orElseThrow(() -> new IllegalStateException("Server not ready")).submitAndJoin(r); +public class StandaloneMessagingFactory extends MessagingFactory { + public StandaloneMessagingFactory(LPStandalonePlugin plugin) { + super(plugin); } @Override - public Executor sync() { - return this.sync; + protected InternalMessagingService getServiceFor(String messagingType) { + return super.getServiceFor(messagingType); } } diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java new file mode 100644 index 000000000..eec85c5b5 --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/StandaloneSenderFactory.java @@ -0,0 +1,82 @@ +/* + * 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 me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.sender.SenderFactory; +import me.lucko.luckperms.standalone.app.integration.StandaloneSender; +import net.kyori.adventure.text.Component; +import net.luckperms.api.util.Tristate; + +import java.util.UUID; + +public class StandaloneSenderFactory extends SenderFactory { + + public StandaloneSenderFactory(LPStandalonePlugin plugin) { + super(plugin); + } + + @Override + protected String getName(StandaloneSender sender) { + return sender.getName(); + } + + @Override + protected UUID getUniqueId(StandaloneSender sender) { + return sender.getUniqueId(); + } + + @Override + protected void sendMessage(StandaloneSender sender, Component message) { + Component rendered = TranslationManager.render(message, sender.getLocale()); + sender.sendMessage(rendered); + } + + @Override + protected Tristate getPermissionValue(StandaloneSender sender, String node) { + return sender.getPermissionValue(node); + } + + @Override + protected boolean hasPermission(StandaloneSender sender, String node) { + return sender.hasPermission(node); + } + + @Override + protected void performCommand(StandaloneSender sender, String command) { + + } + + @Override + protected boolean isConsole(StandaloneSender sender) { + return sender.isConsole(); + } + + @Override + protected boolean shouldSplitNewlines(StandaloneSender sender) { + return true; + } +} diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java new file mode 100644 index 000000000..7becb9feb --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneContextManager.java @@ -0,0 +1,43 @@ +/* + * 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.stub; + +import me.lucko.luckperms.common.context.manager.SimpleContextManager; +import me.lucko.luckperms.standalone.LPStandalonePlugin; +import me.lucko.luckperms.standalone.app.integration.StandaloneSender; + +import java.util.UUID; + +public class StandaloneContextManager extends SimpleContextManager { + public StandaloneContextManager(LPStandalonePlugin plugin) { + super(plugin, StandaloneSender.class, StandaloneSender.class); + } + + @Override + public UUID getUniqueId(StandaloneSender player) { + return player.getUniqueId(); + } +} diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneDummyConnectionListener.java b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneDummyConnectionListener.java new file mode 100644 index 000000000..5ea39d281 --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneDummyConnectionListener.java @@ -0,0 +1,35 @@ +/* + * 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.stub; + +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; + +public class StandaloneDummyConnectionListener extends AbstractConnectionListener { + public StandaloneDummyConnectionListener(LuckPermsPlugin plugin) { + super(plugin); + } +} diff --git a/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneEventBus.java b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneEventBus.java new file mode 100644 index 000000000..506cf3015 --- /dev/null +++ b/standalone/src/main/java/me/lucko/luckperms/standalone/stub/StandaloneEventBus.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.standalone.stub; + +import me.lucko.luckperms.common.api.LuckPermsApiProvider; +import me.lucko.luckperms.common.event.AbstractEventBus; +import me.lucko.luckperms.standalone.LPStandalonePlugin; + +public class StandaloneEventBus extends AbstractEventBus { + public StandaloneEventBus(LPStandalonePlugin plugin, LuckPermsApiProvider apiProvider) { + super(plugin, apiProvider); + } + + @Override + protected Object checkPlugin(Object plugin) throws IllegalArgumentException { + return plugin; + } + +} diff --git a/standalone/src/main/resources/config.yml b/standalone/src/main/resources/config.yml new file mode 100644 index 000000000..2d8aa32d5 --- /dev/null +++ b/standalone/src/main/resources/config.yml @@ -0,0 +1,619 @@ +#################################################################################################### +# +----------------------------------------------------------------------------------------------+ # +# | __ __ ___ __ __ | # +# | | | | / ` |__/ |__) |__ |__) |\/| /__` | # +# | |___ \__/ \__, | \ | |___ | \ | | .__/ | # +# | | # +# | https://luckperms.net | # +# | | # +# | WIKI: https://luckperms.net/wiki | # +# | DISCORD: https://discord.gg/luckperms | # +# | BUG REPORTS: https://github.com/LuckPerms/LuckPerms/issues | # +# | | # +# | Each option in this file is documented and explained here: | # +# | ==> https://luckperms.net/wiki/Configuration | # +# | | # +# | New options are not added to this file automatically. Default values are used if an | # +# | option cannot be found. The latest config versions can be obtained at the link above. | # +# +----------------------------------------------------------------------------------------------+ # +#################################################################################################### + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | ESSENTIAL SETTINGS | # +# | | # +# | Important settings that control how LuckPerms functions. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The name of the server, used for server specific permissions. +# +# - When set to "global" this setting is effectively ignored. +# - In all other cases, the value here is added to all players in a "server" context. +# - See: https://luckperms.net/wiki/Context +server: global + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | STORAGE SETTINGS | # +# | | # +# | Controls which storage method LuckPerms will use to store data. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# How the plugin should store data +# +# - The various options are explained in more detail on the wiki: +# https://luckperms.net/wiki/Storage-types +# +# - Possible options: +# +# | Remote databases - require connection information to be configured below +# |=> MySQL +# |=> MariaDB (preferred over MySQL) +# |=> PostgreSQL +# |=> MongoDB +# +# | Flatfile/local database - don't require any extra configuration +# |=> H2 (preferred over SQLite) +# |=> SQLite +# +# | Readable & editable text files - don't require any extra configuration +# |=> YAML (.yml files) +# |=> JSON (.json files) +# |=> HOCON (.conf files) +# |=> TOML (.toml files) +# | +# | By default, user, group and track data is separated into different files. Data can be combined +# | and all stored in the same file by switching to a combined storage variant. +# | Just add '-combined' to the end of the storage-method, e.g. 'yaml-combined' +# +# - A H2 database is the default option. +# - If you want to edit data manually in "traditional" storage files, we suggest using YAML. +storage-method: h2 + +# The following block defines the settings for remote database storage methods. +# +# - You don't need to touch any of the settings here if you're using a local storage method! +# - The connection detail options are shared between all remote storage types. +data: + + # Define the address and port for the database. + # - The standard DB engine port is used by default + # (MySQL: 3306, PostgreSQL: 5432, MongoDB: 27017) + # - Specify as "host:port" if differs + address: localhost + + # The name of the database to store LuckPerms data in. + # - This must be created already. Don't worry about this setting if you're using MongoDB. + database: minecraft + + # Credentials for the database. + username: root + password: '' + + # These settings apply to the MySQL connection pool. + # - The default values will be suitable for the majority of users. + # - Do not change these settings unless you know what you're doing! + pool-settings: + + # Sets the maximum size of the MySQL connection pool. + # - Basically this value will determine the maximum number of actual + # connections to the database backend. + # - More information about determining the size of connection pools can be found here: + # https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing + maximum-pool-size: 10 + + # Sets the minimum number of idle connections that the pool will try to maintain. + # - For maximum performance and responsiveness to spike demands, it is recommended to not set + # this value and instead allow the pool to act as a fixed size connection pool. + # (set this value to the same as 'maximum-pool-size') + minimum-idle: 10 + + # This setting controls the maximum lifetime of a connection in the pool in milliseconds. + # - The value should be at least 30 seconds less than any database or infrastructure imposed + # connection time limit. + maximum-lifetime: 1800000 # 30 minutes + + # This setting controls how frequently the pool will 'ping' a connection in order to prevent it + # from being timed out by the database or network infrastructure, measured in milliseconds. + # - The value should be less than maximum-lifetime and greater than 30000 (30 seconds). + # - Setting the value to zero will disable the keepalive functionality. + keepalive-time: 0 + + # This setting controls the maximum number of milliseconds that the plugin will wait for a + # connection from the pool, before timing out. + connection-timeout: 5000 # 5 seconds + + # This setting allows you to define extra properties for connections. + # + # By default, the following options are set to enable utf8 encoding. (you may need to remove + # these if you are using PostgreSQL) + # useUnicode: true + # characterEncoding: utf8 + # + # You can also use this section to disable SSL connections, by uncommenting the 'useSSL' and + # 'verifyServerCertificate' options below. + properties: + useUnicode: true + characterEncoding: utf8 + #useSSL: false + #verifyServerCertificate: false + + # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). + # - Change this if you want to use different tables for different servers. + table-prefix: 'luckperms_' + + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. + mongodb-collection-prefix: '' + + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ + mongodb-connection-uri: '' + +# Define settings for a "split" storage setup. +# +# - This allows you to define a storage method for each type of data. +# - The connection options above still have to be correct for each type here. +split-storage: + # Don't touch this if you don't want to use split storage! + enabled: false + methods: + # These options don't need to be modified if split storage isn't enabled. + user: h2 + group: h2 + track: h2 + uuid: h2 + log: h2 + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | UPDATE PROPAGATION & MESSAGING SERVICE | # +# | | # +# | Controls the ways in which LuckPerms will sync data & notify other servers of changes. | # +# | These options are documented on greater detail on the wiki under "Instant Updates". | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# This option controls how frequently LuckPerms will perform a sync task. +# +# - A sync task will refresh all data from the storage, and ensure that the most up-to-date data is +# being used by the plugin. +# - This is disabled by default, as most users will not need it. However, if you're using a remote +# storage type without a messaging service setup, you may wish to set this to something like 3. +# - Set to -1 to disable the task completely. +sync-minutes: -1 + +# If the file watcher should be enabled. +# +# - When using a file-based storage type, LuckPerms can monitor the data files for changes, and +# automatically update when changes are detected. +# - If you don't want this feature to be active, set this option to false. +watch-files: true + +# Define which messaging service should be used by the plugin. +# +# - If enabled and configured, LuckPerms will use the messaging service to inform other connected +# servers of changes. +# - Use the command "/lp networksync" to manually push changes. +# - Data is NOT stored using this service. It is only used as a messaging platform. +# +# - If you decide to enable this feature, you should set "sync-minutes" to -1, as there is no need +# for LuckPerms to poll the database for changes. +# +# - Possible options: +# => sql Uses the SQL database to form a queue system for communication. Will only work when +# 'storage-method' is set to MySQL or MariaDB. This is chosen by default if the +# option is set to 'auto' and SQL storage is in use. Set to 'notsql' to disable this. +# => redis Uses Redis pub-sub to push changes. Your server connection info must be configured +# below. +# => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be +# configured below. +# => nats Uses Nats pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. +# => auto Attempts to automatically setup a messaging service using redis or sql. +messaging-service: auto + +# If LuckPerms should automatically push updates after a change has been made with a command. +auto-push-updates: true + +# If LuckPerms should push logging entries to connected servers via the messaging service. +push-log-entries: true + +# If LuckPerms should broadcast received logging entries to players on this platform. +# +# - If you have LuckPerms installed on your backend servers as well as a BungeeCord proxy, you +# should set this option to false on either your backends or your proxies, to avoid players being +# messaged twice about log entries. +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 + username: '' + password: '' + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel: + enabled: false + master: mymaster + addresses: + - localhost:26379 + username: '' + password: '' + +# Settings for Nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats: + enabled: false + address: localhost + username: '' + password: '' + token: '' + +# Settings for RabbitMQ. +# Port 5672 is used by default; set address to "host:port" if differs +rabbitmq: + enabled: false + address: localhost + vhost: '/' + username: 'guest' + password: 'guest' + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | CUSTOMIZATION SETTINGS | # +# | | # +# | Settings that allow admins to customize the way LuckPerms operates. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# Controls how temporary permissions/parents/meta should be accumulated. +# +# - The default behaviour is "deny". +# - This behaviour can also be specified when the command is executed. See the command usage +# documentation for more info. +# +# - Possible options: +# => accumulate durations will be added to the existing expiry time +# => replace durations will be replaced if the new duration is later than the current +# expiration +# => deny the command will just fail if you try to add another node with the same expiry +temporary-add-behaviour: deny + +# Controls how LuckPerms will determine a users "primary" group. +# +# - The meaning and influence of "primary groups" are explained in detail on the wiki. +# - The preferred approach is to let LuckPerms automatically determine a users primary group +# based on the relative weight of their parent groups. +# +# - Possible options: +# => stored use the value stored against the users record in the file/database +# => parents-by-weight just use the users most highly weighted parent +# => all-parents-by-weight same as above, but calculates based upon all parents inherited from +# both directly and indirectly +primary-group-calculation: parents-by-weight + +# If the plugin should check for "extra" permissions with users run LP commands. +# +# - These extra permissions allow finer control over what users can do with each command, and who +# they have access to edit. +# - The nature of the checks are documented on the wiki under "Argument based command permissions". +# - Argument based permissions are *not* static, unlike the 'base' permissions, and will depend upon +# the arguments given within the command. +argument-based-command-permissions: false + +# If the plugin should check whether senders are a member of a given group before they're able to +# edit the groups data or add/remove other users to/from it. +# Note: these limitations do not apply to the web editor! +require-sender-group-membership-to-modify: false + +# If the plugin should send log notifications to users whenever permissions are modified. +# +# - Notifications are only sent to those with the appropriate permission to receive them +# - They can also be temporarily enabled/disabled on a per-user basis using +# '/lp log notify ' +log-notify: true + +# Defines a list of log entries which should not be sent as notifications to users. +# +# - Each entry in the list is a RegEx expression which is matched against the log entry description. +log-notify-filtered-descriptions: +# - "parent add example" + +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations: true + +# Defines the options for prefix and suffix stacking. +# +# - The feature allows you to display multiple prefixes or suffixes alongside a players username in +# chat. +# - It is explained and documented in more detail on the wiki under "Prefix & Suffix Stacking". +# +# - The options are divided into separate sections for prefixes and suffixes. +# - The 'duplicates' setting refers to how duplicate elements are handled. Can be 'retain-all', +# 'first-only' or 'last-only'. +# - The value of 'start-spacer' is included at the start of the resultant prefix/suffix. +# - The value of 'end-spacer' is included at the end of the resultant prefix/suffix. +# - The value of 'middle-spacer' is included between each element in the resultant prefix/suffix. +# +# - Possible format options: +# => highest Selects the value with the highest weight, from all values +# held by or inherited by the player. +# +# => lowest Same as above, except takes the one with the lowest weight. +# +# => highest_own Selects the value with the highest weight, but will not +# accept any inherited values. +# +# => lowest_own Same as above, except takes the value with the lowest weight. +# +# => highest_inherited Selects the value with the highest weight, but will only +# accept inherited values. +# +# => lowest_inherited Same as above, except takes the value with the lowest weight. +# +# => highest_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group on the given track. +# +# => lowest_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_on_track_ Selects the value with the highest weight, but only if the +# value was inherited from a group not on the given track. +# +# => lowest_not_on_track_ Same as above, except takes the value with the lowest weight. +# +# => highest_from_group_ Selects the value with the highest weight, but only if the +# value was inherited from the given group. +# +# => lowest_from_group_ Same as above, except takes the value with the lowest weight. +# +# => highest_not_from_group_ Selects the value with the highest weight, but only if the +# value was not inherited from the given group. +# +# => lowest_not_from_group_ Same as above, except takes the value with the lowest weight. +meta-formatting: + prefix: + format: + - "highest" + duplicates: first-only + start-spacer: "" + middle-spacer: " " + end-spacer: "" + suffix: + format: + - "highest" + duplicates: first-only + start-spacer: "" + middle-spacer: " " + end-spacer: "" + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | PERMISSION CALCULATION AND INHERITANCE | # +# | | # +# | Modify the way permission checks, meta lookups and inheritance resolutions are handled. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# The algorithm LuckPerms should use when traversing the "inheritance tree". +# +# - Possible options: +# => breadth-first See: https://en.wikipedia.org/wiki/Breadth-first_search +# => depth-first-pre-order See: https://en.wikipedia.org/wiki/Depth-first_search +# => depth-first-post-order See: https://en.wikipedia.org/wiki/Depth-first_search +inheritance-traversal-algorithm: depth-first-pre-order + +# If a final sort according to "inheritance rules" should be performed after the traversal algorithm +# has resolved the inheritance tree. +# +# "Inheritance rules" refers to things such as group weightings, primary group status, and the +# natural contextual ordering of the group nodes. +# +# Setting this to 'true' will allow for the inheritance rules to take priority over the structure of +# the inheritance tree. +# +# Effectively when this setting is 'true': the tree is flattened, and rules applied afterwards, +# and when this setting is 'false':, the rules are just applied during each step of the traversal. +post-traversal-inheritance-sort: false + +# Defines the mode used to determine whether a set of contexts are satisfied. +# +# - Possible options: +# => at-least-one-value-per-key Set A will be satisfied by another set B, if at least one of the +# key-value entries per key in A are also in B. +# => all-values-per-key Set A will be satisfied by another set B, if all key-value +# entries in A are also in B. +context-satisfy-mode: at-least-one-value-per-key + +# +----------------------------------------------------------------------------------------------+ # +# | Permission resolution settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If users on this server should have their global permissions applied. +# When set to false, only server specific permissions will apply for users on this server +include-global: true + +# If users on this server should have their global world permissions applied. +# When set to false, only world specific permissions will apply for users on this server +include-global-world: true + +# If users on this server should have global (non-server specific) groups applied +apply-global-groups: true + +# If users on this server should have global (non-world specific) groups applied +apply-global-world-groups: true + +# +----------------------------------------------------------------------------------------------+ # +# | Meta lookup settings | # +# +----------------------------------------------------------------------------------------------+ # + +# Defines how meta values should be selected. +# +# - Possible options: +# => inheritance Selects the meta value that was inherited first +# => highest-number Selects the highest numerical meta value +# => lowest-number Selects the lowest numerical meta value +meta-value-selection-default: inheritance + +# Defines how meta values should be selected per key. +meta-value-selection: +# max-homes: highest-number + +# +----------------------------------------------------------------------------------------------+ # +# | Inheritance settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If the plugin should apply wildcard permissions. +# +# - If set to true, LuckPerms will detect wildcard permissions, and resolve & apply all registered +# permissions matching the wildcard. +apply-wildcards: true + +# If LuckPerms should resolve and apply permissions according to the Sponge style implicit wildcard +# inheritance system. +# +# - That being: If a user has been granted "example", then the player should have also be +# automatically granted "example.function", "example.another", "example.deeper.nesting", +# and so on. +apply-sponge-implicit-wildcards: false + +# If the plugin should apply negated Bukkit default permissions before it considers wildcard +# assignments. +# +# - Plugin authors can define permissions which explicitly should not be given automatically to OPs. +# This is usually used for so called "anti-permissions" - permissions which, when granted, apply +# something negative. +# - If this option is set to true, LuckPerms will consider any negated declarations made by +# plugins before it considers wildcards. (similar to the way the OP system works) +# - If this option is set to false, LuckPerms will consider any wildcard assignments first. +apply-default-negated-permissions-before-wildcards: false + +# If the plugin should parse regex permissions. +# +# - If set to true, LuckPerms will detect regex permissions, marked with "r=" at the start of the +# node, and resolve & apply all registered permissions matching the regex. +apply-regex: true + +# If the plugin should complete and apply shorthand permissions. +# +# - If set to true, LuckPerms will detect and expand shorthand node patterns. +apply-shorthand: true + +# +----------------------------------------------------------------------------------------------+ # +# | Extra settings | # +# +----------------------------------------------------------------------------------------------+ # + +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators: [] + +# Define special group weights for this server. +# +# - Group weights can also be applied directly to group data, using the setweight command. +# - This section allows weights to be set on a per-server basis. +group-weight: +# admin: 10 + + + + +# +----------------------------------------------------------------------------------------------+ # +# | | # +# | FINE TUNING OPTIONS | # +# | | # +# | A number of more niche settings for tweaking and changing behaviour. The section also | # +# | contains toggles for some more specialised features. It is only necessary to make changes to | # +# | these options if you want to fine-tune LuckPerms behaviour. | # +# | | # +# +----------------------------------------------------------------------------------------------+ # + +# +----------------------------------------------------------------------------------------------+ # +# | Miscellaneous (and rarely used) settings | # +# +----------------------------------------------------------------------------------------------+ # + +# If LuckPerms should allow usernames with non alphanumeric characters. +# +# - Note that due to the design of the storage implementation, usernames must still be 16 characters +# or less. +allow-invalid-usernames: false + +# If LuckPerms should not require users to confirm bulkupdate operations. +# +# - When set to true, operations will be executed immediately. +# - This is not recommended, as bulkupdate has the potential to irreversibly delete large amounts of +# data, and is not designed to be executed automatically. +# - If automation is needed, users should prefer using the LuckPerms API. +skip-bulkupdate-confirmation: false + +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate: false + +# If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. +# +# - When this happens, the plugin will set their primary group back to default. +prevent-primary-group-removal: false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode: + players: false + console: false + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands: + players: false + console: false diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java new file mode 100644 index 000000000..8eec5687f --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/CommandsIntegrationTest.java @@ -0,0 +1,1456 @@ +/* + * 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.common.collect.ImmutableSet; +import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.model.User; +import me.lucko.luckperms.common.node.types.Inheritance; +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import me.lucko.luckperms.standalone.utils.CommandTester; +import me.lucko.luckperms.standalone.utils.TestPluginProvider; +import me.lucko.luckperms.standalone.utils.TestSender; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.luckperms.api.event.log.LogNotifyEvent; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +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 { + + private static final Map CONFIG = ImmutableMap.builder() + .put("log-notify", "false") + .put("commands-rate-limit", "false") + .build(); + + @Test + public void testGroupCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenHasPermissions("luckperms.creategroup") + .whenRunCommand("creategroup test") + .thenExpect("[LP] test was successfully created.") + + .givenHasPermissions("luckperms.creategroup") + .whenRunCommand("creategroup test2") + .thenExpect("[LP] test2 was successfully created.") + + .givenHasPermissions("luckperms.deletegroup") + .whenRunCommand("deletegroup test2") + .thenExpect("[LP] test2 was successfully deleted.") + + .givenHasPermissions("luckperms.listgroups") + .whenRunCommand("listgroups") + .thenExpect(""" + [LP] Showing group entries: (page 1 of 1 - 2 entries) + [LP] Groups: (name, weight, tracks) + [LP] - default - 0 + [LP] - test - 0 + """ + ) + + .givenHasPermissions("luckperms.group.info") + .whenRunCommand("group test info") + .thenExpect(""" + [LP] > Group Info: test + [LP] - Display Name: test + [LP] - Weight: None + [LP] - Contextual Data: (mode: server) + [LP] Prefix: None + [LP] Suffix: None + [LP] Meta: None + """ + ) + + .givenHasPermissions("luckperms.group.meta.set") + .whenRunCommand("group test meta set hello world") + .thenExpect("[LP] Set meta key 'hello' to 'world' for test in context global.") + + .givenHasPermissions("luckperms.group.setweight") + .whenRunCommand("group test setweight 10") + .thenExpect("[LP] Set weight to 10 for group test.") + + .givenHasPermissions("luckperms.group.setweight") + .whenRunCommand("group test setweight 100") + .thenExpect("[LP] Set weight to 100 for group test.") + + .givenHasPermissions("luckperms.group.setdisplayname") + .whenRunCommand("group test setdisplayname Test") + .thenExpect("[LP] Set display name to Test for group test in context global.") + + .givenHasPermissions("luckperms.group.setdisplayname") + .whenRunCommand("group test setdisplayname Dummy") + .thenExpect("[LP] Set display name to Dummy for group test in context global.") + + .givenHasPermissions("luckperms.group.info") + .whenRunCommand("group Dummy info") + .thenExpect(""" + [LP] > Group Info: test + [LP] - Display Name: Dummy + [LP] - Weight: 100 + [LP] - Contextual Data: (mode: server) + [LP] Prefix: None + [LP] Suffix: None + [LP] Meta: (weight=100) (hello=world) + """ + ) + + .givenHasPermissions("luckperms.group.clone") + .whenRunCommand("group test clone testclone") + .thenExpect("[LP] test (Dummy) was successfully cloned onto testclone (Dummy).") + + .givenHasPermissions("luckperms.group.info") + .whenRunCommand("group testclone info") + .thenExpect(""" + [LP] > Group Info: testclone + [LP] - Display Name: Dummy + [LP] - Weight: 100 + [LP] - Contextual Data: (mode: server) + [LP] Prefix: None + [LP] Suffix: None + [LP] Meta: (weight=100) (hello=world) + """ + ) + + .givenHasPermissions("luckperms.group.rename") + .whenRunCommand("group test rename test2") + .thenExpect("[LP] test (Dummy) was successfully renamed to test2 (Dummy).") + + .givenHasPermissions("luckperms.group.info") + .whenRunCommand("group test2 info") + .thenExpect(""" + [LP] > Group Info: test2 + [LP] - Display Name: Dummy + [LP] - Weight: 100 + [LP] - Contextual Data: (mode: server) + [LP] Prefix: None + [LP] Suffix: None + [LP] Meta: (weight=100) (hello=world) + """ + ) + + .givenHasPermissions("luckperms.listgroups") + .whenRunCommand("listgroups") + .thenExpect(""" + [LP] Showing group entries: (page 1 of 1 - 3 entries) + [LP] Groups: (name, weight, tracks) + [LP] - test2 (Dummy) - 100 + [LP] - testclone (Dummy) - 100 + [LP] - default - 0 + """ + ); + }); + } + + @Test + public void testGroupPermissionCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .whenRunCommand("creategroup test") + .clearMessageBuffer() + + .givenHasPermissions("luckperms.group.permission.set") + .whenRunCommand("group test permission set test.node true") + .thenExpect("[LP] Set test.node to true for test in context global.") + + .givenHasPermissions("luckperms.group.permission.set") + .whenRunCommand("group test permission set test.node.other false server=test") + .thenExpect("[LP] Set test.node.other to false for test in context server=test.") + + .givenHasPermissions("luckperms.group.permission.set") + .whenRunCommand("group test permission set test.node.other false server=test world=test2") + .thenExpect("[LP] Set test.node.other to false for test in context server=test, world=test2.") + + .givenHasPermissions("luckperms.group.permission.settemp") + .whenRunCommand("group test permission settemp abc true 1h") + .thenExpect("[LP] Set abc to true for test for a duration of 1 hour in context global.") + + .givenHasPermissions("luckperms.group.permission.settemp") + .whenRunCommand("group test permission settemp abc true 2h replace") + .thenExpect("[LP] Set abc to true for test for a duration of 2 hours in context global.") + + .givenHasPermissions("luckperms.group.permission.unsettemp") + .whenRunCommand("group test permission unsettemp abc") + .thenExpect("[LP] Unset temporary permission abc for test in context global.") + + .givenHasPermissions("luckperms.group.permission.info") + .whenRunCommand("group test permission info") + .thenExpect(""" + [LP] test's Permissions: (page 1 of 1 - 3 entries) + > test.node.other (server=test) (world=test2) + > test.node.other (server=test) + > test.node + """ + ) + + .givenHasPermissions("luckperms.group.permission.unset") + .whenRunCommand("group test permission unset test.node") + .thenExpect("[LP] Unset test.node for test in context global.") + + .givenHasPermissions("luckperms.group.permission.unset") + .whenRunCommand("group test permission unset test.node.other") + .thenExpect("[LP] test does not have test.node.other set in context global.") + + .givenHasPermissions("luckperms.group.permission.unset") + .whenRunCommand("group test permission unset test.node.other server=test") + .thenExpect("[LP] Unset test.node.other for test in context server=test.") + + .givenHasPermissions("luckperms.group.permission.check") + .whenRunCommand("group test permission check test.node.other") + .thenExpect(""" + [LP] Permission information for test.node.other: + [LP] - test has test.node.other set to false in context server=test, world=test2. + [LP] - test does not inherit test.node.other. + [LP] + [LP] Permission check for test.node.other: + [LP] Result: undefined + [LP] Processor: None + [LP] Cause: None + [LP] Context: None + """ + ) + + .givenHasPermissions("luckperms.group.permission.clear") + .whenRunCommand("group test permission clear server=test world=test2") + .thenExpect("[LP] test's permissions were cleared in context server=test, world=test2. (1 node was removed.)") + + .givenHasPermissions("luckperms.group.permission.info") + .whenRunCommand("group test permission info") + .thenExpect("[LP] test does not have any permissions set.") + + .givenHasPermissions("luckperms.group.permission.set") + .whenRunCommand("group test permission set {1-1000} true") + .thenExpect(""" + [LP] Warning: Permission {1-1000} could not be parsed as shorthand: Range between 1 and 1000 exceeds limit of 250 + [LP] Set {1-1000} to true for test in context global. + """ + ); + }); + } + + @Test + public void testGroupParentCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .whenRunCommand("creategroup test") + .whenRunCommand("creategroup test2") + .whenRunCommand("creategroup test3") + .clearMessageBuffer() + + .givenHasPermissions("luckperms.group.parent.add") + .whenRunCommand("group test parent add default") + .thenExpect("[LP] test now inherits permissions from default in context global.") + + .givenHasPermissions("luckperms.group.parent.add") + .whenRunCommand("group test parent add test2 server=test") + .thenExpect("[LP] test now inherits permissions from test2 in context server=test.") + + .givenHasPermissions("luckperms.group.parent.add") + .whenRunCommand("group test parent add test3 server=test") + .thenExpect("[LP] test now inherits permissions from test3 in context server=test.") + + .givenHasPermissions("luckperms.group.parent.addtemp") + .whenRunCommand("group test parent addtemp test2 1d server=hello") + .thenExpect("[LP] test now inherits permissions from test2 for a duration of 1 day in context server=hello.") + + .givenHasPermissions("luckperms.group.parent.removetemp") + .whenRunCommand("group test parent removetemp test2 server=hello") + .thenExpect("[LP] test no longer temporarily inherits permissions from test2 in context server=hello.") + + .givenHasPermissions("luckperms.group.parent.info") + .whenRunCommand("group test parent info") + .thenExpect(""" + [LP] test's Parents: (page 1 of 1 - 3 entries) + > test2 (server=test) + > test3 (server=test) + > default + """ + ) + + .givenHasPermissions("luckperms.group.parent.set") + .whenRunCommand("group test parent set test2 server=test") + .thenExpect("[LP] test had their existing parent groups cleared, and now only inherits test2 in context server=test.") + + .givenHasPermissions("luckperms.group.parent.remove") + .whenRunCommand("group test parent remove test2 server=test") + .thenExpect("[LP] test no longer inherits permissions from test2 in context server=test.") + + .givenHasPermissions("luckperms.group.parent.clear") + .whenRunCommand("group test parent clear") + .thenExpect("[LP] test's parents were cleared in context global. (1 node was removed.)") + + .givenHasPermissions("luckperms.group.parent.info") + .whenRunCommand("group test parent info") + .thenExpect("[LP] test does not have any parents defined."); + }); + } + + @Test + public void testGroupMetaCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .whenRunCommand("creategroup test") + .clearMessageBuffer() + + .givenHasPermissions("luckperms.group.meta.info") + .whenRunCommand("group test meta info") + .thenExpect(""" + [LP] test has no prefixes. + [LP] test has no suffixes. + [LP] test has no meta. + """ + ) + + .givenHasPermissions("luckperms.group.meta.set") + .whenRunCommand("group test meta set hello world") + .thenExpect("[LP] Set meta key 'hello' to 'world' for test in context global.") + + + .givenHasPermissions("luckperms.group.meta.set") + .whenRunCommand("group test meta set hello world2 server=test") + .thenExpect("[LP] Set meta key 'hello' to 'world2' for test in context server=test.") + + .givenHasPermissions("luckperms.group.meta.addprefix") + .whenRunCommand("group test meta addprefix 10 \"&ehello world\"") + .thenExpect("[LP] test had prefix 'hello world' set at a priority of 10 in context global.") + + .givenHasPermissions("luckperms.group.meta.addsuffix") + .whenRunCommand("group test meta addsuffix 100 \"&ehi\"") + .thenExpect("[LP] test had suffix 'hi' set at a priority of 100 in context global.") + + .givenHasPermissions("luckperms.group.meta.addsuffix") + .whenRunCommand("group test meta addsuffix 1 \"&6no\"") + .thenExpect("[LP] test had suffix 'no' set at a priority of 1 in context global.") + + .givenHasPermissions("luckperms.group.meta.settemp") + .whenRunCommand("group test meta settemp abc xyz 1d server=hello") + .thenExpect("[LP] Set meta key 'abc' to 'xyz' for test for a duration of 1 day in context server=hello.") + + .givenHasPermissions("luckperms.group.meta.addtempprefix") + .whenRunCommand("group test meta addtempprefix 1000 abc 1d server=hello") + .thenExpect("[LP] test had prefix 'abc' set at a priority of 1000 for a duration of 1 day in context server=hello.") + + .givenHasPermissions("luckperms.group.meta.addtempsuffix") + .whenRunCommand("group test meta addtempsuffix 1000 xyz 3d server=hello") + .thenExpect("[LP] test had suffix 'xyz' set at a priority of 1000 for a duration of 3 days in context server=hello.") + + .givenHasPermissions("luckperms.group.meta.unsettemp") + .whenRunCommand("group test meta unsettemp abc server=hello") + .thenExpect("[LP] Unset temporary meta key 'abc' for test in context server=hello.") + + .givenHasPermissions("luckperms.group.meta.removetempprefix") + .whenRunCommand("group test meta removetempprefix 1000 abc server=hello") + .thenExpect("[LP] test had temporary prefix 'abc' at priority 1000 removed in context server=hello.") + + .givenHasPermissions("luckperms.group.meta.removetempsuffix") + .whenRunCommand("group test meta removetempsuffix 1000 xyz server=hello") + .thenExpect("[LP] test had temporary suffix 'xyz' at priority 1000 removed in context server=hello.") + + .givenHasPermissions("luckperms.group.meta.info") + .whenRunCommand("group test meta info") + .thenExpect(""" + [LP] test's Prefixes + [LP] -> 10 - 'hello world' (inherited from self) + [LP] test's Suffixes + [LP] -> 100 - 'hi' (inherited from self) + [LP] -> 1 - 'no' (inherited from self) + [LP] test's Meta + [LP] -> hello = 'world2' (inherited from self) (server=test) + [LP] -> hello = 'world' (inherited from self) + """ + ) + + .givenHasPermissions("luckperms.group.info") + .whenRunCommand("group test info") + .thenExpect(""" + [LP] > Group Info: test + [LP] - Display Name: test + [LP] - Weight: None + [LP] - Contextual Data: (mode: server) + [LP] Prefix: "hello world" + [LP] Suffix: "hi" + [LP] Meta: (hello=world) + """ + ) + + .givenHasPermissions("luckperms.group.meta.unset") + .whenRunCommand("group test meta unset hello") + .thenExpect("[LP] Unset meta key 'hello' for test in context global.") + + .givenHasPermissions("luckperms.group.meta.unset") + .whenRunCommand("group test meta unset hello server=test") + .thenExpect("[LP] Unset meta key 'hello' for test in context server=test.") + + .givenHasPermissions("luckperms.group.meta.removeprefix") + .whenRunCommand("group test meta removeprefix 10") + .thenExpect("[LP] test had all prefixes at priority 10 removed in context global.") + + .givenHasPermissions("luckperms.group.meta.removesuffix") + .whenRunCommand("group test meta removesuffix 100") + .thenExpect("[LP] test had all suffixes at priority 100 removed in context global.") + + .givenHasPermissions("luckperms.group.meta.removesuffix") + .whenRunCommand("group test meta removesuffix 1") + .thenExpect("[LP] test had all suffixes at priority 1 removed in context global.") + + .givenHasPermissions("luckperms.group.meta.info") + .whenRunCommand("group test meta info") + .thenExpect(""" + [LP] test has no prefixes. + [LP] test has no suffixes. + [LP] test has no meta. + """ + ); + }); + } + + @Test + public void testUserCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + plugin.getStorage().savePlayerData(UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"), "Notch").join(); + + new CommandTester(executor) + .givenHasPermissions("luckperms.user.info") + .whenRunCommand("user Luck info") + .thenExpect(""" + [LP] > User Info: luck + [LP] - UUID: c1d60c50-70b5-4722-8057-87767557e50d + [LP] (type: official) + [LP] - Status: Offline + [LP] - Parent Groups: + [LP] > default + [LP] - Contextual Data: (mode: server) + [LP] Contexts: None + [LP] Prefix: None + [LP] Suffix: None + [LP] Primary Group: default + [LP] Meta: (primarygroup=default) + """ + ) + + .givenHasPermissions("luckperms.user.info") + .whenRunCommand("user c1d60c50-70b5-4722-8057-87767557e50d info") + .thenExpect(""" + [LP] > User Info: luck + [LP] - UUID: c1d60c50-70b5-4722-8057-87767557e50d + [LP] (type: official) + [LP] - Status: Offline + [LP] - Parent Groups: + [LP] > default + [LP] - Contextual Data: (mode: server) + [LP] Contexts: None + [LP] Prefix: None + [LP] Suffix: None + [LP] Primary Group: default + [LP] Meta: (primarygroup=default) + """ + ) + + .givenHasAllPermissions() + .whenRunCommand("creategroup admin") + .whenRunCommand("user Luck parent set admin") + .clearMessageBuffer() + + .givenHasPermissions("luckperms.user.clone") + .whenRunCommand("user Luck clone Notch") + .thenExpect("[LP] luck was successfully cloned onto notch.") + + .givenHasPermissions("luckperms.user.parent.info") + .whenRunCommand("user Notch parent info") + .thenExpect(""" + [LP] notch's Parents: (page 1 of 1 - 1 entries) + > admin + """ + ); + }); + } + + @Test + public void testUserPermissionCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + new CommandTester(executor) + .givenHasPermissions("luckperms.user.permission.set") + .whenRunCommand("user Luck permission set test.node true") + .thenExpect("[LP] Set test.node to true for luck in context global.") + + .givenHasPermissions("luckperms.user.permission.set") + .whenRunCommand("user Luck permission set test.node.other false server=test") + .thenExpect("[LP] Set test.node.other to false for luck in context server=test.") + + .givenHasPermissions("luckperms.user.permission.set") + .whenRunCommand("user Luck permission set test.node.other false server=test world=test2") + .thenExpect("[LP] Set test.node.other to false for luck in context server=test, world=test2.") + + .givenHasPermissions("luckperms.user.permission.settemp") + .whenRunCommand("user Luck permission settemp abc true 1h") + .thenExpect("[LP] Set abc to true for luck for a duration of 1 hour in context global.") + + .givenHasPermissions("luckperms.user.permission.settemp") + .whenRunCommand("user Luck permission settemp abc true 2h replace") + .thenExpect("[LP] Set abc to true for luck for a duration of 2 hours in context global.") + + .givenHasPermissions("luckperms.user.permission.unsettemp") + .whenRunCommand("user Luck permission unsettemp abc") + .thenExpect("[LP] Unset temporary permission abc for luck in context global.") + + .givenHasPermissions("luckperms.user.permission.info") + .whenRunCommand("user Luck permission info") + .thenExpect(""" + [LP] luck's Permissions: (page 1 of 1 - 3 entries) + > test.node.other (server=test) (world=test2) + > test.node.other (server=test) + > test.node + """ + ) + + .givenHasPermissions("luckperms.user.permission.unset") + .whenRunCommand("user Luck permission unset test.node") + .thenExpect("[LP] Unset test.node for luck in context global.") + + .givenHasPermissions("luckperms.user.permission.unset") + .whenRunCommand("user Luck permission unset test.node.other") + .thenExpect("[LP] luck does not have test.node.other set in context global.") + + .givenHasPermissions("luckperms.user.permission.unset") + .whenRunCommand("user Luck permission unset test.node.other server=test") + .thenExpect("[LP] Unset test.node.other for luck in context server=test.") + + .givenHasPermissions("luckperms.user.permission.check") + .whenRunCommand("user Luck permission check test.node.other") + .thenExpect(""" + [LP] Permission information for test.node.other: + [LP] - luck has test.node.other set to false in context server=test, world=test2. + [LP] - luck does not inherit test.node.other. + [LP] + [LP] Permission check for test.node.other: + [LP] Result: undefined + [LP] Processor: None + [LP] Cause: None + [LP] Context: None + """ + ) + + .givenHasPermissions("luckperms.user.permission.clear") + .whenRunCommand("user Luck permission clear server=test world=test2") + .thenExpect("[LP] luck's permissions were cleared in context server=test, world=test2. (1 node was removed.)") + + .givenHasPermissions("luckperms.user.permission.info") + .whenRunCommand("user Luck permission info") + .thenExpect("[LP] luck does not have any permissions set."); + }); + } + + @Test + public void testUserParentCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + new CommandTester(executor) + .whenRunCommand("creategroup test2") + .whenRunCommand("creategroup test3") + .clearMessageBuffer() + + .givenHasPermissions("luckperms.user.parent.add") + .whenRunCommand("user Luck parent add default") + .thenExpect("[LP] luck already inherits from default in context global.") + + .givenHasPermissions("luckperms.user.parent.add") + .whenRunCommand("user Luck parent add test2 server=test") + .thenExpect("[LP] luck now inherits permissions from test2 in context server=test.") + + .givenHasPermissions("luckperms.user.parent.add") + .whenRunCommand("user Luck parent add test3 server=test") + .thenExpect("[LP] luck now inherits permissions from test3 in context server=test.") + + .givenHasPermissions("luckperms.user.parent.addtemp") + .whenRunCommand("user Luck parent addtemp test2 1d server=hello") + .thenExpect("[LP] luck now inherits permissions from test2 for a duration of 1 day in context server=hello.") + + .givenHasPermissions("luckperms.user.parent.removetemp") + .whenRunCommand("user Luck parent removetemp test2 server=hello") + .thenExpect("[LP] luck no longer temporarily inherits permissions from test2 in context server=hello.") + + .givenHasPermissions("luckperms.user.parent.info") + .whenRunCommand("user Luck parent info") + .thenExpect(""" + [LP] luck's Parents: (page 1 of 1 - 3 entries) + > test2 (server=test) + > test3 (server=test) + > default + """ + ) + + .givenHasPermissions("luckperms.user.parent.set") + .whenRunCommand("user Luck parent set test2 server=test") + .thenExpect("[LP] luck had their existing parent groups cleared, and now only inherits test2 in context server=test.") + + .givenHasPermissions("luckperms.user.parent.remove") + .whenRunCommand("user Luck parent remove test2 server=test") + .thenExpect("[LP] luck no longer inherits permissions from test2 in context server=test.") + + .givenHasPermissions("luckperms.user.parent.clear") + .whenRunCommand("user Luck parent clear") + .thenExpect("[LP] luck's parents were cleared in context global. (0 nodes were removed.)") + + .givenHasPermissions("luckperms.user.parent.info") + .whenRunCommand("user Luck parent info") + .thenExpect(""" + [LP] luck's Parents: (page 1 of 1 - 1 entries) + > default + """ + ); + }); + } + + @Test + public void testUserMetaCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + new CommandTester(executor) + .givenHasPermissions("luckperms.user.meta.info") + .whenRunCommand("user Luck meta info") + .thenExpect(""" + [LP] luck has no prefixes. + [LP] luck has no suffixes. + [LP] luck has no meta. + """ + ) + + .givenHasPermissions("luckperms.user.meta.set") + .whenRunCommand("user Luck meta set hello world") + .thenExpect("[LP] Set meta key 'hello' to 'world' for luck in context global.") + + .givenHasPermissions("luckperms.user.meta.set") + .whenRunCommand("user Luck meta set hello world2 server=test") + .thenExpect("[LP] Set meta key 'hello' to 'world2' for luck in context server=test.") + + .givenHasPermissions("luckperms.user.meta.addprefix") + .whenRunCommand("user Luck meta addprefix 10 \"&ehello world\"") + .thenExpect("[LP] luck had prefix 'hello world' set at a priority of 10 in context global.") + + .givenHasPermissions("luckperms.user.meta.addsuffix") + .whenRunCommand("user Luck meta addsuffix 100 \"&ehi\"") + .thenExpect("[LP] luck had suffix 'hi' set at a priority of 100 in context global.") + + .givenHasPermissions("luckperms.user.meta.addsuffix") + .whenRunCommand("user Luck meta addsuffix 1 \"&6no\"") + .thenExpect("[LP] luck had suffix 'no' set at a priority of 1 in context global.") + + .givenHasPermissions("luckperms.user.meta.settemp") + .whenRunCommand("user Luck meta settemp abc xyz 1d server=hello") + .thenExpect("[LP] Set meta key 'abc' to 'xyz' for luck for a duration of 1 day in context server=hello.") + + .givenHasPermissions("luckperms.user.meta.addtempprefix") + .whenRunCommand("user Luck meta addtempprefix 1000 abc 1d server=hello") + .thenExpect("[LP] luck had prefix 'abc' set at a priority of 1000 for a duration of 1 day in context server=hello.") + + .givenHasPermissions("luckperms.user.meta.addtempsuffix") + .whenRunCommand("user Luck meta addtempsuffix 1000 xyz 3d server=hello") + .thenExpect("[LP] luck had suffix 'xyz' set at a priority of 1000 for a duration of 3 days in context server=hello.") + + .givenHasPermissions("luckperms.user.meta.unsettemp") + .whenRunCommand("user Luck meta unsettemp abc server=hello") + .thenExpect("[LP] Unset temporary meta key 'abc' for luck in context server=hello.") + + .givenHasPermissions("luckperms.user.meta.removetempprefix") + .whenRunCommand("user Luck meta removetempprefix 1000 abc server=hello") + .thenExpect("[LP] luck had temporary prefix 'abc' at priority 1000 removed in context server=hello.") + + .givenHasPermissions("luckperms.user.meta.removetempsuffix") + .whenRunCommand("user Luck meta removetempsuffix 1000 xyz server=hello") + .thenExpect("[LP] luck had temporary suffix 'xyz' at priority 1000 removed in context server=hello.") + + .givenHasPermissions("luckperms.user.meta.info") + .whenRunCommand("user Luck meta info") + .thenExpect(""" + [LP] luck's Prefixes + [LP] -> 10 - 'hello world' (inherited from self) + [LP] luck's Suffixes + [LP] -> 100 - 'hi' (inherited from self) + [LP] -> 1 - 'no' (inherited from self) + [LP] luck's Meta + [LP] -> hello = 'world2' (inherited from self) (server=test) + [LP] -> hello = 'world' (inherited from self) + """ + ) + + .givenHasPermissions("luckperms.user.info") + .whenRunCommand("user Luck info") + .thenExpect(""" + [LP] > User Info: luck + [LP] - UUID: c1d60c50-70b5-4722-8057-87767557e50d + [LP] (type: official) + [LP] - Status: Offline + [LP] - Parent Groups: + [LP] > default + [LP] - Contextual Data: (mode: server) + [LP] Contexts: None + [LP] Prefix: "hello world" + [LP] Suffix: "hi" + [LP] Primary Group: default + [LP] Meta: (hello=world) (primarygroup=default) + """ + ) + + .givenHasPermissions("luckperms.user.meta.unset") + .whenRunCommand("user Luck meta unset hello") + .thenExpect("[LP] Unset meta key 'hello' for luck in context global.") + + .givenHasPermissions("luckperms.user.meta.unset") + .whenRunCommand("user Luck meta unset hello server=test") + .thenExpect("[LP] Unset meta key 'hello' for luck in context server=test.") + + .givenHasPermissions("luckperms.user.meta.removeprefix") + .whenRunCommand("user Luck meta removeprefix 10") + .thenExpect("[LP] luck had all prefixes at priority 10 removed in context global.") + + .givenHasPermissions("luckperms.user.meta.removesuffix") + .whenRunCommand("user Luck meta removesuffix 100") + .thenExpect("[LP] luck had all suffixes at priority 100 removed in context global.") + + .givenHasPermissions("luckperms.user.meta.removesuffix") + .whenRunCommand("user Luck meta removesuffix 1") + .thenExpect("[LP] luck had all suffixes at priority 1 removed in context global.") + + .givenHasPermissions("luckperms.user.meta.info") + .whenRunCommand("user Luck meta info") + .thenExpect(""" + [LP] luck has no prefixes. + [LP] luck has no suffixes. + [LP] luck has no meta. + """ + ); + }); + } + + @Test + public void testTrackCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenHasPermissions("luckperms.createtrack") + .whenRunCommand("createtrack test1") + .thenExpect("[LP] test1 was successfully created.") + + .givenHasPermissions("luckperms.createtrack") + .whenRunCommand("createtrack test2") + .thenExpect("[LP] test2 was successfully created.") + + .givenHasPermissions("luckperms.listtracks") + .whenRunCommand("listtracks") + .thenExpect("[LP] Tracks: test1, test2") + + .givenHasPermissions("luckperms.deletetrack") + .whenRunCommand("deletetrack test2") + .thenExpect("[LP] test2 was successfully deleted.") + + .givenHasAllPermissions() + .whenRunCommand("creategroup aaa") + .whenRunCommand("creategroup bbb") + .whenRunCommand("creategroup ccc") + .clearMessageBuffer() + + .givenHasPermissions("luckperms.track.append") + .whenRunCommand("track test1 append bbb") + .thenExpect("[LP] Group bbb was appended to track test1.") + + .givenHasPermissions("luckperms.track.insert") + .whenRunCommand("track test1 insert aaa 1") + .thenExpect(""" + [LP] Group aaa was inserted into track test1 at position 1. + [LP] aaa ---> bbb + """ + ) + + .givenHasPermissions("luckperms.track.insert") + .whenRunCommand("track test1 insert ccc 3") + .thenExpect(""" + [LP] Group ccc was inserted into track test1 at position 3. + [LP] aaa ---> bbb ---> ccc + """ + ) + + .givenHasPermissions("luckperms.track.info") + .whenRunCommand("track test1 info") + .thenExpect(""" + [LP] > Showing Track: test1 + [LP] - Path: aaa ---> bbb ---> ccc + """ + ) + + .givenHasPermissions("luckperms.track.clone") + .whenRunCommand("track test1 clone testclone") + .thenExpect("[LP] test1 was successfully cloned onto testclone.") + + .givenHasPermissions("luckperms.track.info") + .whenRunCommand("track testclone info") + .thenExpect(""" + [LP] > Showing Track: testclone + [LP] - Path: aaa ---> bbb ---> ccc + """ + ) + + .givenHasPermissions("luckperms.track.rename") + .whenRunCommand("track test1 rename test2") + .thenExpect("[LP] test1 was successfully renamed to test2.") + + .givenHasPermissions("luckperms.listtracks") + .whenRunCommand("listtracks") + .thenExpect("[LP] Tracks: test2, testclone") + + .givenHasPermissions("luckperms.track.info") + .whenRunCommand("track test2 info") + .thenExpect(""" + [LP] > Showing Track: test2 + [LP] - Path: aaa ---> bbb ---> ccc + """ + ) + + .givenHasPermissions("luckperms.group.showtracks") + .whenRunCommand("group aaa showtracks") + .thenExpect(""" + [LP] aaa's Tracks: + > test2: + (aaa ---> bbb ---> ccc) + > testclone: + (aaa ---> bbb ---> ccc) + """ + ) + + .givenHasPermissions("luckperms.track.remove") + .whenRunCommand("track test2 remove bbb") + .thenExpect(""" + [LP] Group bbb was removed from track test2. + [LP] aaa ---> ccc + """ + ) + + .givenHasPermissions("luckperms.track.clear") + .whenRunCommand("track test2 clear") + .thenExpect("[LP] test2's groups track was cleared."); + }); + } + + @Test + public void testUserTrackCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + new CommandTester(executor) + .whenRunCommand("createtrack staff") + .whenRunCommand("createtrack premium") + + .whenRunCommand("creategroup mod") + .whenRunCommand("creategroup admin") + + .whenRunCommand("creategroup vip") + .whenRunCommand("creategroup vip+") + .whenRunCommand("creategroup mvp") + .whenRunCommand("creategroup mvp+") + + .whenRunCommand("track staff append mod") + .whenRunCommand("track staff append admin") + .whenRunCommand("track premium append vip") + .whenRunCommand("track premium append vip+") + .whenRunCommand("track premium append mvp") + .whenRunCommand("track premium append mvp+") + + .clearMessageBuffer() + + .givenHasPermissions("luckperms.user.promote") + .whenRunCommand("user Luck promote staff") + .thenExpect("[LP] luck isn't in any groups on staff, so they were added to the first group, mod in context global.") + + .givenHasPermissions("luckperms.user.promote") + .whenRunCommand("user Luck promote staff") + .thenExpect(""" + [LP] Promoting luck along track staff from mod to admin in context global. + [LP] mod ---> admin + """ + ) + + .givenHasPermissions("luckperms.user.promote") + .whenRunCommand("user Luck promote staff") + .thenExpect("[LP] The end of track staff was reached, unable to promote luck.") + + .givenHasPermissions("luckperms.user.demote") + .whenRunCommand("user Luck demote staff") + .thenExpect(""" + [LP] Demoting luck along track staff from admin to mod in context global. + [LP] mod <--- admin + """ + ) + + .givenHasPermissions("luckperms.user.demote") + .whenRunCommand("user Luck demote staff") + .thenExpect("[LP] The end of track staff was reached, so luck was removed from mod.") + + .givenHasPermissions("luckperms.user.demote") + .whenRunCommand("user Luck demote staff") + .thenExpect("[LP] luck isn't already in any groups on staff.") + + .givenHasPermissions("luckperms.user.promote") + .whenRunCommand("user Luck promote premium server=test1") + .thenExpect("[LP] luck isn't in any groups on premium, so they were added to the first group, vip in context server=test1.") + + .givenHasPermissions("luckperms.user.promote") + .whenRunCommand("user Luck promote premium server=test2") + .thenExpect("[LP] luck isn't in any groups on premium, so they were added to the first group, vip in context server=test2.") + + .givenHasPermissions("luckperms.user.promote") + .whenRunCommand("user Luck promote premium server=test1") + .thenExpect(""" + [LP] Promoting luck along track premium from vip to vip+ in context server=test1. + [LP] vip ---> vip+ ---> mvp ---> mvp+ + """ + ) + + .givenHasPermissions("luckperms.user.promote") + .whenRunCommand("user Luck promote premium server=test2") + .thenExpect(""" + [LP] Promoting luck along track premium from vip to vip+ in context server=test2. + [LP] vip ---> vip+ ---> mvp ---> mvp+ + """ + ) + + .givenHasPermissions("luckperms.user.parent.info") + .whenRunCommand("user Luck parent info") + .thenExpect(""" + [LP] luck's Parents: (page 1 of 1 - 3 entries) + > vip+ (server=test2) + > vip+ (server=test1) + > default + """ + ) + + .givenHasPermissions("luckperms.user.showtracks") + .whenRunCommand("user Luck showtracks") + .thenExpect(""" + [LP] luck's Tracks: + > premium: (server=test2) + (vip ---> vip+ ---> mvp ---> mvp+) + > premium: (server=test1) + (vip ---> vip+ ---> mvp ---> mvp+) + """ + ) + + .givenHasPermissions("luckperms.user.demote") + .whenRunCommand("user Luck demote premium server=test1") + .thenExpect(""" + [LP] Demoting luck along track premium from vip+ to vip in context server=test1. + [LP] vip <--- vip+ <--- mvp <--- mvp+ + """ + ) + + .givenHasPermissions("luckperms.user.demote") + .whenRunCommand("user Luck demote premium server=test2") + .thenExpect(""" + [LP] Demoting luck along track premium from vip+ to vip in context server=test2. + [LP] vip <--- vip+ <--- mvp <--- mvp+ + """ + ) + + .givenHasPermissions("luckperms.user.demote") + .whenRunCommand("user Luck demote premium server=test1") + .thenExpect("[LP] The end of track premium was reached, so luck was removed from vip.") + + .givenHasPermissions("luckperms.user.demote") + .whenRunCommand("user Luck demote premium server=test2") + .thenExpect("[LP] The end of track premium was reached, so luck was removed from vip.") + + .givenHasPermissions("luckperms.user.parent.info") + .whenRunCommand("user Luck parent info") + .thenExpect(""" + [LP] luck's Parents: (page 1 of 1 - 1 entries) + > default + """ + ); + }); + } + + @Test + public void testSearchCommand(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + new CommandTester(executor) + .whenRunCommand("creategroup test") + .whenRunCommand("user Luck permission set hello.world true server=survival") + .whenRunCommand("group test permission set hello.world true world=nether") + .whenRunCommand("user Luck parent add test") + .clearMessageBuffer() + + .givenHasPermissions("luckperms.search") + .whenRunCommand("search hello.world") + .thenExpect(""" + [LP] Searching for users and groups with permissions == hello.world... + [LP] Found 2 entries from 1 users and 1 groups. + [LP] Showing user entries: (page 1 of 1 - 1 entries) + > luck - true (server=survival) + [LP] Showing group entries: (page 1 of 1 - 1 entries) + > test - true (world=nether) + """ + ) + + .givenHasPermissions("luckperms.search") + .whenRunCommand("search ~~ group.%") + .thenExpect(""" + [LP] Searching for users and groups with permissions ~~ group.%... + [LP] Found 2 entries from 2 users and 0 groups. + [LP] Showing user entries: (page 1 of 1 - 2 entries) + > luck - (group.test) - true + > luck - (group.default) - true + """ + ); + }); + } + + @Test + public void testBulkUpdate(@TempDir Path tempDir) throws InterruptedException { + Map config = new HashMap<>(CONFIG); + config.put("skip-bulkupdate-confirmation", "true"); + + TestPluginProvider.use(tempDir, config, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + plugin.getStorage().savePlayerData(UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"), "Notch").join(); + + CountDownLatch completed = new CountDownLatch(1); + + TestSender console = new TestSender(); + console.setConsole(true); + console.addMessageSink(component -> { + String plain = PlainTextComponentSerializer.plainText().serialize(component); + if (plain.contains("Bulk update completed successfully")) { + completed.countDown(); + } + }); + + new CommandTester(executor, console) + .whenRunCommand("creategroup moderator") + .whenRunCommand("creategroup admin") + .whenRunCommand("user Luck parent add moderator server=survival") + .whenRunCommand("user Notch parent add moderator") + .whenRunCommand("group admin parent add moderator") + .whenRunCommand("group moderator rename mod") + .clearMessageBuffer() + + .whenRunCommand("bulkupdate all update permission group.mod \"permission == group.moderator\"") + .thenExpectStartsWith("[LP] Running bulk update."); + + assertTrue(completed.await(15, TimeUnit.SECONDS), "operation did not complete in the allotted time"); + + Group adminGroup = plugin.getGroupManager().getIfLoaded("admin"); + assertNotNull(adminGroup); + assertEquals(ImmutableSet.of(Inheritance.builder("mod").build()), adminGroup.normalData().asSet()); + + User luckUser = plugin.getStorage().loadUser(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), null).join(); + assertNotNull(luckUser); + assertEquals( + ImmutableSet.of( + Inheritance.builder("default").build(), + Inheritance.builder("mod").withContext("server", "survival").build() + ), + luckUser.normalData().asSet() + ); + + User notchUser = plugin.getStorage().loadUser(UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"), null).join(); + assertNotNull(notchUser); + assertEquals( + ImmutableSet.of( + Inheritance.builder("default").build(), + Inheritance.builder("mod").build() + ), + notchUser.normalData().asSet() + ); + + // test normal players are unable to use + new CommandTester(executor) + .givenHasPermissions("luckperms.bulkupdate") + .whenRunCommand("bulkupdate all update permission group.mod \"permission == group.moderator\"") + .thenExpect("[LP] The bulk update command can only be used from the console."); + }); + } + + @Test + public void testLogCommands(@TempDir Path tempDir) { + Map config = new HashMap<>(CONFIG); + config.put("log-notify", "true"); + config.put("log-synchronously-in-commands", "true"); + + TestPluginProvider.use(tempDir, config, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + UUID luckUniqueId = UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"); + + plugin.getStorage().savePlayerData(luckUniqueId, "Luck").join(); + plugin.getStorage().savePlayerData(UUID.fromString("069a79f4-44e9-4726-a5be-fca90e38aaf5"), "Notch").join(); + + TestSender testSender = new TestSender(); + testSender.setUniqueId(luckUniqueId); + testSender.setName("Luck"); + + new CommandTester(executor, testSender) + .whenRunCommand("group default permission set hello"); + + new CommandTester(executor) + .whenRunCommand("creategroup moderator") + .whenRunCommand("creategroup admin") + .whenRunCommand("createtrack staff") + .whenRunCommand("user Luck parent add moderator server=survival") + .whenRunCommand("user Notch parent add moderator") + .whenRunCommand("group admin parent add moderator") + .whenRunCommand("group moderator rename mod") + .whenRunCommand("user Luck permission set test.1") + .whenRunCommand("user Luck permission set test.2 false") + .whenRunCommand("user Luck permission set test.3 server=survival") + .whenRunCommand("user Luck permission settemp test.4 true 1h") + .whenRunCommand("user Luck permission settemp test.5 false 2d") + .clearMessageBuffer() + + .givenHasPermissions("luckperms.log.userhistory") + .whenRunCommand("log userhistory Luck") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing history for user luck (page 1 of 1) + [LP] #1 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission settemp test.5 false 2d + [LP] #2 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission settemp test.4 true 1h + [LP] #3 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.3 true server=survival + [LP] #4 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.2 false + [LP] #5 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.1 true + [LP] #6 (1m ago) (StandaloneUser) [U] (luck) + [LP] > parent add moderator server=survival + """ + ) + + .givenHasPermissions("luckperms.log.grouphistory") + .whenRunCommand("log grouphistory admin") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing history for group admin (page 1 of 1) + [LP] #1 (1m ago) (StandaloneUser) [G] (admin) + [LP] > parent add moderator + [LP] #2 (1m ago) (StandaloneUser) [G] (admin) + [LP] > create + """ + ) + + .givenHasPermissions("luckperms.log.trackhistory") + .whenRunCommand("log trackhistory staff") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing history for track staff (page 1 of 1) + [LP] #1 (1m ago) (StandaloneUser) [T] (staff) + [LP] > create + """ + ) + + .givenHasPermissions("luckperms.log.recent") + .whenRunCommand("log recent") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing recent actions (page 1 of 2) + [LP] #1 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission settemp test.5 false 2d + [LP] #2 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission settemp test.4 true 1h + [LP] #3 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.3 true server=survival + [LP] #4 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.2 false + [LP] #5 (1m ago) (StandaloneUser) [U] (luck) + [LP] > permission set test.1 true + [LP] #6 (1m ago) (StandaloneUser) [G] (moderator) + [LP] > rename mod + [LP] #7 (1m ago) (StandaloneUser) [G] (admin) + [LP] > parent add moderator + [LP] #8 (1m ago) (StandaloneUser) [U] (notch) + [LP] > parent add moderator + [LP] #9 (1m ago) (StandaloneUser) [U] (luck) + [LP] > parent add moderator server=survival + [LP] #10 (1m ago) (StandaloneUser) [T] (staff) + [LP] > create + """ + ) + + .givenHasPermissions("luckperms.log.recent") + .whenRunCommand("log recent 2") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing recent actions (page 2 of 2) + [LP] #11 (1m ago) (StandaloneUser) [G] (admin) + [LP] > create + [LP] #12 (1m ago) (StandaloneUser) [G] (moderator) + [LP] > create + [LP] #13 (1m ago) (Luck) [G] (default) + [LP] > permission set hello true + """ + ) + + .givenHasPermissions("luckperms.log.recent") + .whenRunCommand("log recent 3") + .thenExpect("[LP] Invalid page number. Please enter a value between 1 and 2.") + + .givenHasPermissions("luckperms.log.search") + .whenRunCommand("log search hello") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing recent actions for query hello (page 1 of 1) + [LP] #1 (1m ago) (Luck) [G] (default) + [LP] > permission set hello true + """ + ) + + .givenHasPermissions("luckperms.log.recent") + .whenRunCommand("log recent Luck") + .thenExpectReplacing("\\ds ago", "1m ago", """ + [LP] Showing recent actions by Luck (page 1 of 1) + [LP] #1 (1m ago) (Luck) [G] (default) + [LP] > permission set hello true + """ + ); + }); + } + + @Test + public void testInvalidCommands(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenHasPermissions("luckperms.user.info") + .whenRunCommand("user unknown info") + .thenExpect("[LP] A user for unknown could not be found.") + + .givenHasPermissions("luckperms.user.info") + .whenRunCommand("user unknown unknown") + .thenExpect("[LP] Command not recognised.") + + .givenHasPermissions("luckperms.group.info") + .whenRunCommand("group unknown info") + .thenExpect("[LP] A group named unknown could not be found.") + + .givenHasPermissions("luckperms.group.info") + .whenRunCommand("group unknown unknown") + .thenExpect("[LP] Command not recognised.") + + .givenHasPermissions("luckperms.track.info") + .whenRunCommand("track unknown info") + .thenExpect("[LP] A track named unknown could not be found.") + + .givenHasPermissions("luckperms.track.info") + .whenRunCommand("track unknown unknown") + .thenExpect("[LP] Command not recognised."); + }); + } + + @Test + public void testNoPermissions(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, CONFIG, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + String version = "v" + bootstrap.getVersion(); + + new CommandTester(executor) + .givenHasPermissions(/* empty */) + + .whenRunCommand("") + .thenExpect(""" + [LP] Running LuckPerms %s. + [LP] It seems that no permissions have been setup yet! + [LP] Before you can use any of the LuckPerms commands in-game, you need to use the console to give yourself access. + [LP] Open your console and run: + [LP] > lp user StandaloneUser permission set luckperms.* true + [LP] After you've done this, you can begin to define your permission assignments and groups. + [LP] Don't know where to start? Check here: https://luckperms.net/wiki/Usage + """.formatted(version) + ) + + .whenRunCommand("help") + .thenExpect("[LP] Running LuckPerms %s.".formatted(version)) + + .whenRunCommand("group default info") + .thenExpect("[LP] Running LuckPerms %s.".formatted(version)); + }); + } + + @Test + public void testLogNotify(@TempDir Path tempDir) { + Map config = new HashMap<>(CONFIG); + config.put("log-notify", "true"); + + TestPluginProvider.use(tempDir, config, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + // by default, notifications are not sent to the user who initiated the event - override that + app.getApi().getEventBus().subscribe(LogNotifyEvent.class, e -> e.setCancelled(false)); + + TestSender sender = new TestSender(); + plugin.addOnlineSender(sender); + + new CommandTester(executor, sender) + .givenHasPermissions("luckperms.group.permission.set", "luckperms.log.notify") + .whenRunCommand("group default permission set hello.world true server=test") + .thenExpect(""" + [LP] Set hello.world to true for default in context server=test. + [LP] LOG > (StandaloneUser) [G] (default) + [LP] LOG > permission set hello.world true server=test + """ + ); + }); + } + + @Test + public void testArgumentBasedCommandPermissions(@TempDir Path tempDir) { + Map config = new HashMap<>(CONFIG); + config.put("argument-based-command-permissions", "true"); + + TestPluginProvider.use(tempDir, config, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenHasPermissions("luckperms.group.permission.set") + .whenRunCommand("group default permission set hello.world true server=test") + .thenExpect("[LP] You do not have permission to use this command!") + + .givenHasPermissions( + "luckperms.group.permission.set", + "luckperms.group.permission.set.modify.default", + "luckperms.group.permission.set.usecontext.global", + "luckperms.group.permission.set.test.permission" + ) + .whenRunCommand("group default permission set test.permission") + .thenExpect("[LP] Set test.permission to true for default in context global.") + + .givenHasPermissions( + "luckperms.group.permission.unset", + "luckperms.group.permission.unset.modify.default", + "luckperms.group.permission.unset.usecontext.global", + "luckperms.group.permission.unset.test.permission" + ) + .whenRunCommand("group default permission unset test.permission") + .thenExpect("[LP] Unset test.permission for default in context global.") + + .givenHasPermissions( + "luckperms.group.permission.set", + "luckperms.group.permission.set.modify.default", + "luckperms.group.permission.set.usecontext.server.test", + "luckperms.group.permission.set.hello.world" + ) + .whenRunCommand("group default permission set hello.world true server=test") + .thenExpect("[LP] Set hello.world to true for default in context server=test.") + + .givenHasPermissions("luckperms.group.permission.info") + .whenRunCommand("group default permission info") + .thenExpect("[LP] You do not have permission to use this command!") + + .givenHasPermissions( + "luckperms.group.permission.info", + "luckperms.group.permission.info.view.default" + ) + .whenRunCommand("group default permission info") + .thenExpect(""" + [LP] default's Permissions: (page 1 of 1 - 1 entries) + > hello.world (server=test) + """ + ); + }); + } + + @Test + public void testReadOnlyMode(@TempDir Path tempDir) { + Map config = new HashMap<>(CONFIG); + config.put("commands-read-only-mode.players", "true"); + + TestPluginProvider.use(tempDir, config, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenHasPermissions("luckperms.group.permission.info") + .whenRunCommand("group default permission info") + .thenExpect("[LP] default does not have any permissions set.") + + .givenHasPermissions("luckperms.group.permission.info", "luckperms.group.permission.set") + .whenRunCommand("group default permission set test true") + .thenExpect("[LP] You do not have permission to use this command!", false) + + .givenHasAllPermissions() + .whenRunCommand("group default permission set test true") + .thenExpect("[LP] You do not have permission to use this command!"); + }); + } + + @Test + public void testCommandsDisabled(@TempDir Path tempDir) { + Map config = new HashMap<>(CONFIG); + config.put("disable-luckperms-commands.players", "true"); + + TestPluginProvider.use(tempDir, config, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + new CommandTester(executor) + .givenHasAllPermissions() + .whenRunCommand("info") + .thenExpect("[LP] LuckPerms commands are disabled."); + }); + } + +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java new file mode 100644 index 000000000..cea228030 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/ImportExportIntegrationTest.java @@ -0,0 +1,121 @@ +/* + * 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.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import me.lucko.luckperms.common.commands.misc.ExportCommand; +import me.lucko.luckperms.common.commands.misc.ImportCommand; +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.types.Inheritance; +import me.lucko.luckperms.common.node.types.Permission; +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import me.lucko.luckperms.standalone.utils.TestPluginProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; + +import static org.awaitility.Awaitility.await; +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 { + + @Test + public void testRoundTrip(@TempDir Path tempDirA, @TempDir Path tempDirB) throws IOException { + Path path = tempDirA.resolve("testfile.json.gz"); + + // run an export on environment A + TestPluginProvider.use(tempDirA, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + + plugin.getStorage().savePlayerData(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), "Luck").join(); + + executor.execute("creategroup test").join(); + executor.execute("group test permission set test.permission true").join(); + executor.execute("createtrack test").join(); + executor.execute("track test append default").join(); + executor.execute("user Luck permission set hello").join(); + + executor.execute("export testfile").join(); + + ExportCommand exportCommand = (ExportCommand) plugin.getCommandManager().getMainCommands().get("export"); + await().atMost(10, TimeUnit.SECONDS).until(() -> !exportCommand.isRunning()); + }); + + // check the export contains the expected data + try (BufferedReader reader = new BufferedReader(new InputStreamReader(new GZIPInputStream(Files.newInputStream(path))))) { + JsonObject obj = new Gson().fromJson(reader, JsonObject.class); + assertEquals(2, obj.get("groups").getAsJsonObject().size()); + assertEquals(1, obj.get("users").getAsJsonObject().size()); + assertEquals(1, obj.get("tracks").getAsJsonObject().size()); + } + + // copy the export file from environment A to environment B + Files.copy(path, tempDirB.resolve("testfile.json.gz")); + + // import the file on environment B + TestPluginProvider.use(tempDirB, (app, bootstrap, plugin) -> { + CommandExecutor executor = app.getCommandExecutor(); + assertNull(plugin.getGroupManager().getIfLoaded("test")); + + executor.execute("import testfile").join(); + + ImportCommand importCommand = (ImportCommand) plugin.getCommandManager().getMainCommands().get("import"); + await().atMost(10, TimeUnit.SECONDS).until(() -> !importCommand.isRunning()); + + // assert that the expected objects exist + Group testGroup = plugin.getGroupManager().getIfLoaded("test"); + assertNotNull(testGroup); + assertEquals(ImmutableList.of(Permission.builder().permission("test.permission").build()), testGroup.normalData().asList()); + + Track testTrack = plugin.getTrackManager().getIfLoaded("test"); + assertNotNull(testTrack); + assertEquals(ImmutableList.of("default"), testTrack.getGroups()); + + User testUser = plugin.getStorage().loadUser(UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"), null).join(); + assertNotNull(testUser); + assertEquals("luck", testUser.getUsername().orElse(null)); + assertEquals(ImmutableSet.of( + Permission.builder().permission("hello").build(), + Inheritance.builder().group("default").build() + ), testUser.normalData().asSet()); + }); + } +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java new file mode 100644 index 000000000..4184c447d --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/IntegrationTest.java @@ -0,0 +1,100 @@ +/* + * 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 me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.model.Group; +import me.lucko.luckperms.common.node.types.Permission; +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import me.lucko.luckperms.standalone.utils.TestPluginProvider; +import net.luckperms.api.model.data.DataType; +import net.luckperms.api.node.NodeEqualityPredicate; +import net.luckperms.api.platform.Health; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +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. + */ +public class IntegrationTest { + + @Test + public void testLoadEnableDisable(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + Health health = plugin.runHealthCheck(); + assertNotNull(health); + assertTrue(health.isHealthy()); + }); + } + + @Test + public void testRunCommand(@TempDir Path tempDir) { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + CommandExecutor commandExecutor = app.getCommandExecutor(); + commandExecutor.execute("group default permission set test").join(); + + Group group = bootstrap.getPlugin().getStorage().loadGroup("default").join().orElse(null); + assertNotNull(group); + assertTrue(group.hasNode(DataType.NORMAL, Permission.builder().permission("test").build(), NodeEqualityPredicate.EXACT).asBoolean()); + }); + } + + @Test + public void testReloadConfig(@TempDir Path tempDir) throws IOException { + TestPluginProvider.use(tempDir, (app, bootstrap, plugin) -> { + String server = plugin.getConfiguration().get(ConfigKeys.SERVER); + assertEquals("global", server); + + Integer syncTime = plugin.getConfiguration().get(ConfigKeys.SYNC_TIME); + assertEquals(-1, syncTime); + + Path config = tempDir.resolve("config.yml"); + assertTrue(Files.exists(config)); + + String configString = Files.readString(config) + .replace("server: global", "server: test") + .replace("sync-minutes: -1", "sync-minutes: 10"); + Files.writeString(config, configString); + + plugin.getConfiguration().reload(); + + server = plugin.getConfiguration().get(ConfigKeys.SERVER); + assertEquals("test", server); // changed + + syncTime = plugin.getConfiguration().get(ConfigKeys.SYNC_TIME); + assertEquals(-1, syncTime); // unchanged + }); + } + +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java new file mode 100644 index 000000000..beaa604f8 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/MessagingIntegrationTest.java @@ -0,0 +1,296 @@ +/* + * 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 me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.messaging.InternalMessagingService; +import me.lucko.luckperms.common.model.User; +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.messaging.CustomMessageReceiveEvent; +import net.luckperms.api.event.sync.PreNetworkSyncEvent; +import net.luckperms.api.event.sync.SyncType; +import net.luckperms.api.platform.Health; +import org.junit.jupiter.api.Nested; +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.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +@Tag("docker") +public class MessagingIntegrationTest { + + private static void testMessaging(Map config, Path tempDirA, Path tempDirB) throws InterruptedException { + try (TestPluginProvider.Plugin pluginA = TestPluginProvider.create(tempDirA, config); + TestPluginProvider.Plugin pluginB = TestPluginProvider.create(tempDirB, config)) { + + // check the plugins are healthy + Health healthA = pluginA.plugin().runHealthCheck(); + assertNotNull(healthA); + assertTrue(healthA.isHealthy()); + + Health healthB = pluginB.plugin().runHealthCheck(); + assertNotNull(healthB); + assertTrue(healthB.isHealthy()); + + InternalMessagingService messagingServiceA = pluginA.plugin().getMessagingService().orElse(null); + InternalMessagingService messagingServiceB = pluginB.plugin().getMessagingService().orElse(null); + assertNotNull(messagingServiceA); + assertNotNull(messagingServiceB); + + 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(); + + UUID exampleUniqueId = UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"); + String exampleUsername = "Luck"; + User user = pluginA.plugin().getStorage().loadUser(exampleUniqueId, exampleUsername).join(); + + EventBus eventBus = pluginB.app().getApi().getEventBus(); + + CountDownLatch latch1 = new CountDownLatch(1); + eventBus.subscribe(PreNetworkSyncEvent.class, e -> { + if (e.getType() == SyncType.FULL) { + latch1.countDown(); + e.setCancelled(true); + } + }); + + CountDownLatch latch2 = new CountDownLatch(1); + eventBus.subscribe(PreNetworkSyncEvent.class, e -> { + if (e.getType() == SyncType.SPECIFIC_USER && exampleUniqueId.equals(e.getSpecificUserUniqueId())) { + latch2.countDown(); + e.setCancelled(true); + } + }); + + CountDownLatch latch3 = new CountDownLatch(1); + eventBus.subscribe(LogReceiveEvent.class, e -> { + if (e.getEntry().equals(exampleLogEntry)) { + latch3.countDown(); + } + }); + + CountDownLatch latch4 = new CountDownLatch(1); + eventBus.subscribe(CustomMessageReceiveEvent.class, e -> { + if (e.getChannelId().equals("luckperms:test") && e.getPayload().equals("hello")) { + latch4.countDown(); + } + }); + + // send some messages from plugin A to plugin B + messagingServiceA.pushUpdate(); + messagingServiceA.pushUserUpdate(user); + messagingServiceA.pushLog(exampleLogEntry); + messagingServiceA.pushCustomPayload("luckperms:test", "hello"); + + // wait for the messages to be sent/received + assertTrue(latch1.await(10, TimeUnit.SECONDS)); + assertTrue(latch2.await(10, TimeUnit.SECONDS)); + assertTrue(latch3.await(10, TimeUnit.SECONDS)); + assertTrue(latch4.await(10, TimeUnit.SECONDS)); + } + } + + @Nested + class MySql { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("mysql:8")) + .withEnv("MYSQL_DATABASE", "minecraft") + .withEnv("MYSQL_ROOT_PASSWORD", "passw0rd") + .withExposedPorts(3306); + + @Test + public void testMySql(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("storage-method", "mysql") + .put("data.address", host + ":" + port) + .put("data.database", "minecraft") + .put("data.username", "root") + .put("data.password", "passw0rd") + .build(); + + testMessaging(config, tempDirA, tempDirB); + } + } + + @Nested + class MariaDb { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("mariadb")) + .withEnv("MARIADB_USER", "minecraft") + .withEnv("MARIADB_PASSWORD", "passw0rd") + .withEnv("MARIADB_ROOT_PASSWORD", "rootpassw0rd") + .withEnv("MARIADB_DATABASE", "minecraft") + .withExposedPorts(3306); + + @Test + public void testMySql(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("storage-method", "mariadb") + .put("data.address", host + ":" + port) + .put("data.database", "minecraft") + .put("data.username", "minecraft") + .put("data.password", "passw0rd") + .build(); + + testMessaging(config, tempDirA, tempDirB); + } + } + + @Nested + class Postgres { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("postgres")) + .withEnv("POSTGRES_PASSWORD", "passw0rd") + .withExposedPorts(5432); + + @Test + public void testPostgres(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("storage-method", "postgresql") + .put("data.address", host + ":" + port) + .put("data.database", "postgres") + .put("data.username", "postgres") + .put("data.password", "passw0rd") + .build(); + + testMessaging(config, tempDirA, tempDirB); + } + } + + @Nested + class Redis { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("redis")) + .withExposedPorts(6379); + + @Test + public void testRedis(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("messaging-service", "redis") + .put("redis.enabled", "true") + .put("redis.address", host + ":" + port) + .build(); + + testMessaging(config, tempDirA, tempDirB); + } + } + + @Nested + class RabbitMq { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("rabbitmq")) + .withExposedPorts(5672); + + @Test + public void testRabbitMq(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("messaging-service", "rabbitmq") + .put("rabbitmq.enabled", "true") + .put("rabbitmq.address", host + ":" + port) + .build(); + + testMessaging(config, tempDirA, tempDirB); + } + } + + @Nested + class Nats { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("nats")) + .withExposedPorts(4222); + + @Test + public void testNats(@TempDir Path tempDirA, @TempDir Path tempDirB) throws InterruptedException { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("messaging-service", "nats") + .put("nats.enabled", "true") + .put("nats.address", host + ":" + port) + .build(); + + testMessaging(config, tempDirA, tempDirB); + } + } + +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java new file mode 100644 index 000000000..4e0017c7d --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/StorageIntegrationTest.java @@ -0,0 +1,508 @@ +/* + * 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.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import me.lucko.luckperms.common.actionlog.LogPage; +import me.lucko.luckperms.common.actionlog.LoggedAction; +import me.lucko.luckperms.common.filter.FilterList; +import me.lucko.luckperms.common.filter.PageParameters; +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.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.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.NodeType; +import net.luckperms.api.node.types.PrefixNode; +import net.luckperms.api.platform.Health; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.Month; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +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 { + + private static final Node TEST_PERMISSION_1 = Permission.builder() + .permission("example.permission") + .build(); + + private static final Node TEST_PERMISSION_2 = Permission.builder() + .permission("test") + .value(false) + .expiry(LocalDate.of(2050, Month.APRIL, 1).atStartOfDay().toInstant(ZoneOffset.UTC)) + .withContext("server", "foo") + .withContext("world", "bar") + .withContext("test", "test") + .build(); + + private static final Node TEST_GROUP = Inheritance.builder() + .group("default") + .value(false) + .expiry(LocalDate.of(2050, Month.APRIL, 1).atStartOfDay().toInstant(ZoneOffset.UTC)) + .withContext("server", "foo") + .withContext("world", "bar") + .withContext("test", "test") + .build(); + + private static final Node TEST_PREFIX = Prefix.builder() + .priority(100) + .prefix("TEST") + .withContext("server", "foo") + .withContext("world", "bar") + .build(); + + private static final Node TEST_META = Meta.builder() + .key("foo") + .value("bar") + .build(); + + + private static void testStorage(LuckPermsApplication app, TestPluginBootstrap bootstrap, TestPlugin plugin) { + // check the plugin is healthy + Health health = plugin.runHealthCheck(); + assertNotNull(health); + assertTrue(health.isHealthy()); + + // try to create / save a group + Group group = plugin.getStorage().createAndLoadGroup("test", CreationCause.INTERNAL).join(); + group.setNode(DataType.NORMAL, TEST_PERMISSION_1, true); + group.setNode(DataType.NORMAL, TEST_PERMISSION_2, true); + group.setNode(DataType.NORMAL, TEST_GROUP, true); + group.setNode(DataType.NORMAL, TEST_PREFIX, true); + 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).join(); + + // try to create / save a user + UUID exampleUniqueId = UUID.fromString("c1d60c50-70b5-4722-8057-87767557e50d"); + String exampleUsername = "Luck"; + + PlayerSaveResult saveResult = plugin.getStorage().savePlayerData(exampleUniqueId, exampleUsername).join(); + assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT), saveResult.getOutcomes()); + assertNull(saveResult.getOtherUniqueIds()); + assertNull(saveResult.getPreviousUsername()); + + 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); + user.setNode(DataType.NORMAL, TEST_PREFIX, true); + user.setNode(DataType.NORMAL, TEST_META, true); + plugin.getStorage().saveUser(user).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); + assertEquals(ImmutableSet.of(TEST_PERMISSION_1, TEST_PERMISSION_2, TEST_GROUP, TEST_PREFIX, TEST_META), testGroup.normalData().asSet()); + + 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"))); + + Track testTrack = plugin.getTrackManager().getIfLoaded("example"); + assertNotNull(testTrack); + assertEquals(ImmutableList.of("default", "test"), track.getGroups()); + + LogPage actionLog = plugin.getStorage().getLogPage(FilterList.empty(), new PageParameters(1000, 1)).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))); + + 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"; + + 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()); + + + // test savePlayerData + saveResult = plugin.getStorage().savePlayerData(exampleUniqueId, exampleUsername).join(); + assertEquals(ImmutableSet.of(Outcome.NO_CHANGE), saveResult.getOutcomes()); + assertNull(saveResult.getOtherUniqueIds()); + assertNull(saveResult.getPreviousUsername()); + + saveResult = plugin.getStorage().savePlayerData(exampleUniqueId, "test").join(); + assertEquals(ImmutableSet.of(Outcome.USERNAME_UPDATED), saveResult.getOutcomes()); + assertNull(saveResult.getOtherUniqueIds()); + assertTrue(exampleUsername.equalsIgnoreCase(saveResult.getPreviousUsername())); + assertNull(plugin.getStorage().getPlayerUniqueId(exampleUsername).join()); + assertTrue("test".equalsIgnoreCase(plugin.getStorage().getPlayerName(exampleUniqueId).join())); + + saveResult = plugin.getStorage().savePlayerData(otherExampleUniqueId, "test").join(); + assertEquals(ImmutableSet.of(Outcome.CLEAN_INSERT, Outcome.OTHER_UNIQUE_IDS_PRESENT_FOR_USERNAME), saveResult.getOutcomes()); + assertEquals(ImmutableSet.of(exampleUniqueId), saveResult.getOtherUniqueIds()); + assertNull(saveResult.getPreviousUsername()); + assertEquals(otherExampleUniqueId, plugin.getStorage().getPlayerUniqueId("test").join()); + assertNull(plugin.getStorage().getPlayerName(exampleUniqueId).join()); + } + + @Nested + @Tag("docker") + class MySql { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("mysql:8")) + .withEnv("MYSQL_DATABASE", "minecraft") + .withEnv("MYSQL_ROOT_PASSWORD", "passw0rd") + .withExposedPorts(3306); + + @Test + public void testMySql(@TempDir Path tempDir) { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("storage-method", "mysql") + .put("data.address", host + ":" + port) + .put("data.database", "minecraft") + .put("data.username", "root") + .put("data.password", "passw0rd") + .build(); + + TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage); + } + } + + @Nested + @Tag("docker") + class MariaDb { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("mariadb")) + .withEnv("MARIADB_USER", "minecraft") + .withEnv("MARIADB_PASSWORD", "passw0rd") + .withEnv("MARIADB_ROOT_PASSWORD", "rootpassw0rd") + .withEnv("MARIADB_DATABASE", "minecraft") + .withExposedPorts(3306); + + @Test + public void testMariaDb(@TempDir Path tempDir) { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("storage-method", "mariadb") + .put("data.address", host + ":" + port) + .put("data.database", "minecraft") + .put("data.username", "minecraft") + .put("data.password", "passw0rd") + .build(); + + TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage); + } + } + + @Nested + @Tag("docker") + class Postgres { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("postgres")) + .withEnv("POSTGRES_PASSWORD", "passw0rd") + .withExposedPorts(5432); + + @Test + public void testPostgres(@TempDir Path tempDir) { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("storage-method", "postgresql") + .put("data.address", host + ":" + port) + .put("data.database", "postgres") + .put("data.username", "postgres") + .put("data.password", "passw0rd") + .build(); + + TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage); + } + } + + @Nested + @Tag("docker") + class MongoDb { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("mongo")) + .withExposedPorts(27017); + + @Test + public void testMongo(@TempDir Path tempDir) { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("storage-method", "mongodb") + .put("data.address", host + ":" + port) + .put("data.database", "minecraft") + .put("data.username", "") + .put("data.password", "") + .build(); + + TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage); + } + } + + @Nested + @Tag("docker") + class Rest { + + @Container + private final GenericContainer container = new GenericContainer<>(DockerImageName.parse("ghcr.io/luckperms/rest-api")) + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(StorageIntegrationTest.class))) + .withExposedPorts(8080) + .waitingFor(new WaitAllStrategy() + .withStrategy(Wait.forListeningPort()) + .withStrategy(Wait.forLogMessage(".*Successfully enabled.*", 1)) + ); + + @Test + public void testRest(@TempDir Path tempDir) { + assertTrue(this.container.isRunning()); + + String host = this.container.getHost(); + Integer port = this.container.getFirstMappedPort(); + + Map config = ImmutableMap.builder() + .put("storage-method", "rest") + .put("data.rest-url", "http://" + host + ":" + port + "/") + .build(); + + TestPluginProvider.use(tempDir, config, StorageIntegrationTest::testStorage); + } + } + + @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 { + + @Test + public void testYaml(@TempDir Path tempDir) throws IOException { + TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "yaml"), StorageIntegrationTest::testStorage); + + 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"); + } + + @Test + public void testJson(@TempDir Path tempDir) throws IOException { + TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "json"), StorageIntegrationTest::testStorage); + + 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"); + } + + @Test + public void testHocon(@TempDir Path tempDir) throws IOException { + TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "hocon"), StorageIntegrationTest::testStorage); + + 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); + } + + @Test + public void testYamlCombined(@TempDir Path tempDir) throws IOException { + TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "yaml-combined"), StorageIntegrationTest::testStorage); + + Path storageDir = tempDir.resolve("yaml-storage"); + compareFiles(storageDir, "example/yaml-combined", "groups.yml"); + compareFiles(storageDir, "example/yaml-combined", "tracks.yml"); + compareFiles(storageDir, "example/yaml-combined", "users.yml"); + } + + @Test + public void testJsonCombined(@TempDir Path tempDir) throws IOException { + TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "json-combined"), StorageIntegrationTest::testStorage); + + Path storageDir = tempDir.resolve("json-storage"); + compareFiles(storageDir, "example/json-combined", "groups.json"); + compareFiles(storageDir, "example/json-combined", "tracks.json"); + compareFiles(storageDir, "example/json-combined", "users.json"); + } + + @Test + public void testHoconCombined(@TempDir Path tempDir) throws IOException { + TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "hocon-combined"), StorageIntegrationTest::testStorage); + + Path storageDir = tempDir.resolve("hocon-storage"); + compareFiles(storageDir, "example/hocon-combined", "groups.conf"); + compareFiles(storageDir, "example/hocon-combined", "tracks.conf"); + compareFiles(storageDir, "example/hocon-combined", "users.conf"); + } + + @Test + public void testTomlCombined(@TempDir Path tempDir) throws IOException { + TestPluginProvider.use(tempDir, ImmutableMap.of("storage-method", "toml-combined"), StorageIntegrationTest::testStorage); + } + + private static void compareFiles(Path dir, String examplePath, String file) throws IOException { + String exampleFile = examplePath + "/" + file; + + String expected; + try (InputStream in = StorageIntegrationTest.class.getClassLoader().getResourceAsStream(exampleFile)) { + if (in == null) { + throw new IOException("File does not exist: " + exampleFile); + } + expected = new String(in.readAllBytes(), StandardCharsets.UTF_8); + } + + String actual = Files.readString(dir.resolve(Paths.get(file))); + assertEquals(expected.trim(), actual.trim()); + } + + } + +} 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..ccddab8ae --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/WebEditorIntegrationTest.java @@ -0,0 +1,163 @@ +/* + * 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.StandaloneUser; +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 bytesocksUrl = "http://" + this.bytesocks.getHost() + ":" + this.bytesocks.getFirstMappedPort() + "/"; + + Map config = ImmutableMap.builder() + .put("bytebin-url", bytebinUrl) + .put("bytesocks-url", bytesocksUrl) + .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(StandaloneUser.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(bytesocksUrl + 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/CommandTester.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java new file mode 100644 index 000000000..852fd1bff --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/CommandTester.java @@ -0,0 +1,273 @@ +/* + * 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.utils; + +import me.lucko.luckperms.standalone.app.integration.CommandExecutor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.luckperms.api.util.Tristate; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.intellij.lang.annotations.RegExp; + +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; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Utility for testing LuckPerms commands with BDD-like given/when/then assertions. + */ +public final class CommandTester implements Consumer, Function { + + private static final Logger LOGGER = LogManager.getLogger(CommandTester.class); + + /** The LuckPerms command executor */ + private final CommandExecutor executor; + + /** The test player */ + private final TestSender sender; + + /** The current map of permissions held by the fake executor */ + private Map permissions = null; + + /** A set of the permissions that have been checked for */ + private final Set checkedPermissions = Collections.synchronizedSet(new HashSet<>()); + + /** A buffer of messages received by the test tool */ + private final List messageBuffer = Collections.synchronizedList(new ArrayList<>()); + + public CommandTester(CommandExecutor executor, TestSender sender) { + this.executor = executor; + this.sender = sender; + + this.sender.setPermissionChecker(this); + this.sender.addMessageSink(this); + } + + public CommandTester(CommandExecutor executor) { + this(executor, new TestSender()); + } + + /** + * Accept a message and add it to the buffer. + * + * @param component the message + */ + @Override + public void accept(Component component) { + this.messageBuffer.add(component); + } + + /** + * Perform a permission check for the fake executor + * + * @param permission the permission + * @return the result of the permission check + */ + @Override + public Tristate apply(String permission) { + if (this.permissions == null) { + this.checkedPermissions.add(permission); + return Tristate.TRUE; + } else { + Tristate result = this.permissions.getOrDefault(permission, Tristate.UNDEFINED); + if (result != Tristate.UNDEFINED) { + this.checkedPermissions.add(permission); + } + return result; + } + } + + /** + * Marks that the fake executor should have all permissions + * + * @return this + */ + public CommandTester givenHasAllPermissions() { + this.permissions = null; + return this; + } + + /** + * Marks that the fake executor should have the given permissions + * + * @return this + */ + public CommandTester givenHasPermissions(String... permissions) { + this.permissions = new HashMap<>(); + for (String permission : permissions) { + this.permissions.put(permission, Tristate.TRUE); + } + return this; + } + + /** + * Execute a command using the {@link CommandExecutor} and capture output to this test instance. + * + * @param command the command to run + * @return this + */ + public CommandTester whenRunCommand(String command) { + LOGGER.info("Executing test command: " + command); + this.executor.execute(this.sender, command).join(); + return this; + } + + /** + * Asserts that the current contents of the message buffer matches the given input string. + * + * @param expected the expected contents + * @return this + */ + public CommandTester thenExpect(String expected) { + return thenExpect(expected, true); + } + + /** + * Asserts that the current contents of the message buffer matches the given input string. + * + * @param expected the expected contents + * @param checkPermissions whether to assert that the exact permissions were checked + * @return this + */ + public CommandTester thenExpect(String expected, boolean checkPermissions) { + String actual = this.renderBuffer(); + assertEquals(expected.trim(), actual.trim()); + + if (checkPermissions && this.permissions != null) { + assertEquals(this.checkedPermissions, this.permissions.keySet()); + } + + return this.clearMessageBuffer(); + } + + /** + * Asserts that the current contents of the message buffer starts with the given input string. + * + * @param expected the expected contents + * @return this + */ + public CommandTester thenExpectStartsWith(String expected) { + String actual = this.renderBuffer(); + assertTrue(actual.trim().startsWith(expected.trim()), "expected '" + actual + "' to start with '" + expected + "'"); + + if (this.permissions != null) { + assertEquals(this.checkedPermissions, this.permissions.keySet()); + } + + return this.clearMessageBuffer(); + } + + /** + * Asserts that the current contents of the message buffer matches the given input string. + * + * @param expected the expected contents + * @return this + */ + public CommandTester thenExpectReplacing(@RegExp String regex, String replacement, String expected) { + String actual = this.renderBuffer().replaceAll(regex, replacement); + assertEquals(expected.trim(), actual.trim()); + + if (this.permissions != null) { + assertEquals(this.checkedPermissions, this.permissions.keySet()); + } + + return this.clearMessageBuffer(); + } + + /** + * Clears the message buffer. + * + * @return this + */ + public CommandTester clearMessageBuffer() { + this.messageBuffer.clear(); + this.checkedPermissions.clear(); + return this; + } + + /** + * Renders the contents of the message buffer as a stream of lines. + * + * @return rendered copy of the buffer + */ + public Stream renderBufferStream() { + return this.messageBuffer.stream().map(component -> PlainTextComponentSerializer.plainText().serialize(component)); + } + + /** + * Renders the contents of the message buffer as a joined string. + * + * @return rendered copy of the buffer + */ + public String renderBuffer() { + return this.renderBufferStream().map(String::trim).collect(Collectors.joining("\n")); + } + + /** + * Prints test case source code to stdout to test the given command. + * + * @param cmd the command + * @return this + */ + public CommandTester outputTest(String cmd) { + this.whenRunCommand(cmd); + + String checkedPermissions = this.checkedPermissions.stream() + .map(s -> "\"" + s + "\"") + .collect(Collectors.joining(", ")); + + System.out.printf(".givenHasPermissions(%s)%n", checkedPermissions); + System.out.printf(".whenRunCommand(\"%s\")%n", cmd); + + List render = this.renderBufferStream().toList(); + if (render.size() == 1) { + System.out.printf(".thenExpect(\"%s\")%n", render.get(0)); + } else { + System.out.println(".thenExpect(\"\"\""); + for (String s : render) { + System.out.println(" " + s); + } + System.out.println(" \"\"\""); + System.out.println(")"); + } + + System.out.println(); + return this.clearMessageBuffer(); + } + +} 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 new file mode 100644 index 000000000..ba8c2a5f6 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginBootstrap.java @@ -0,0 +1,128 @@ +/* + * 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.utils; + +import me.lucko.luckperms.common.dependencies.Dependency; +import me.lucko.luckperms.common.dependencies.DependencyManager; +import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.storage.StorageType; +import me.lucko.luckperms.standalone.LPStandaloneBootstrap; +import me.lucko.luckperms.standalone.LPStandalonePlugin; +import me.lucko.luckperms.standalone.app.LuckPermsApplication; +import me.lucko.luckperms.standalone.app.integration.StandaloneSender; +import me.lucko.luckperms.standalone.app.integration.StandaloneUser; + +import java.nio.file.Path; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.stream.Stream; + +/** + * An extension standalone bootstrap for testing. + * + *

    Key differences:

    + *

    + *

      + *
    • Dependency loading system is replaced with a no-op stub that delegates to the test classloader
    • + *
    • Ability to register additional sender instances as being online
    • + *
    + *

    + */ +public final class TestPluginBootstrap extends LPStandaloneBootstrap { + private static final ClassPathAppender NOOP_APPENDER = file -> {}; + + private final Path dataDirectory; + private TestPlugin plugin; + + TestPluginBootstrap(LuckPermsApplication app, Path dataDirectory) { + super(app, NOOP_APPENDER); + this.dataDirectory = dataDirectory; + } + + public TestPlugin getPlugin() { + return this.plugin; + } + + @Override + public Path getDataDirectory() { + return this.dataDirectory; + } + + @Override + protected LPStandalonePlugin createTestPlugin() { + this.plugin = new TestPlugin(this); + return this.plugin; + } + + public static final class TestPlugin extends LPStandalonePlugin { + private final Set onlineSenders = new CopyOnWriteArraySet<>(); + + TestPlugin(LPStandaloneBootstrap bootstrap) { + super(bootstrap); + } + + @Override + protected DependencyManager createDependencyManager() { + return new TestDependencyManager(); + } + + @Override + public Stream getOnlineSenders() { + return Stream.concat( + Stream.of(StandaloneUser.INSTANCE), + this.onlineSenders.stream() + ).map(player -> getSenderFactory().wrap(player)); + } + + public void addOnlineSender(StandaloneSender player) { + this.onlineSenders.add(player); + } + } + + static final class TestDependencyManager implements DependencyManager { + + @Override + public void loadDependencies(Set dependencies) { + + } + + @Override + public void loadStorageDependencies(Set storageTypes, boolean redis, boolean rabbitmq, boolean nats) { + + } + + @Override + public ClassLoader obtainClassLoaderWith(Set dependencies) { + return getClass().getClassLoader(); + } + + @Override + public void close() { + + } + } +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginProvider.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginProvider.java new file mode 100644 index 000000000..de3f394a1 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestPluginProvider.java @@ -0,0 +1,112 @@ +/* + * 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.utils; + +import com.google.common.collect.ImmutableMap; +import me.lucko.luckperms.standalone.app.LuckPermsApplication; +import me.lucko.luckperms.standalone.utils.TestPluginBootstrap.TestPlugin; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +public final class TestPluginProvider { + private TestPluginProvider() {} + + /** + * Creates a test LuckPerms plugin instance, loads/enables it, and returns it. + * + * @param tempDir the temporary directory to run the plugin in + * @param config the config to set + * @return the plugin + */ + public static Plugin create(Path tempDir, Map config) { + Map props = new HashMap<>(config); + props.putIfAbsent("auto-install-translations", "false"); + props.putIfAbsent("editor-lazily-generate-key", "true"); + + props.forEach((k, v) -> System.setProperty("luckperms." + k, v)); + + LuckPermsApplication app = new LuckPermsApplication(() -> {}); + TestPluginBootstrap bootstrap = new TestPluginBootstrap(app, tempDir); + + bootstrap.onLoad(); + bootstrap.onEnable(); + + props.keySet().forEach((k) -> System.clearProperty("luckperms." + k)); + + return new Plugin(app, bootstrap, bootstrap.getPlugin()); + } + + /** + * Creates a test LuckPerms plugin instance, loads/enables it, and returns it. + * + * @param tempDir the temporary directory to run the plugin in + * @return the plugin + */ + public static Plugin create(Path tempDir) { + return create(tempDir, ImmutableMap.of()); + } + + /** + * Creates a test LuckPerms plugin instance, loads/enables it, runs the consumer, then disables it. + * + * @param tempDir the temporary directory to run the plugin in + * @param config the config to set + * @param consumer the consumer + * @param the exception class thrown by the consumer + * @throws E exception + */ + public static void use(Path tempDir, Map config, Consumer consumer) throws E { + try (Plugin plugin = create(tempDir, config)) { + consumer.accept(plugin.app, plugin.bootstrap, plugin.plugin); + } + } + + /** + * Creates a test LuckPerms plugin instance, loads/enables it, runs the consumer, then disables it. + * + * @param tempDir the temporary directory to run the plugin in + * @param consumer the consumer + * @param the exception class thrown by the consumer + * @throws E exception + */ + public static void use(Path tempDir, Consumer consumer) throws E { + use(tempDir, ImmutableMap.of(), consumer); + } + + public interface Consumer { + void accept(LuckPermsApplication app, TestPluginBootstrap bootstrap, TestPlugin plugin) throws E; + } + + public record Plugin(LuckPermsApplication app, TestPluginBootstrap bootstrap, TestPlugin plugin) implements AutoCloseable { + @Override + public void close() { + this.bootstrap.onDisable(); + } + } + +} diff --git a/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestSender.java b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestSender.java new file mode 100644 index 000000000..e1723dea5 --- /dev/null +++ b/standalone/src/test/java/me/lucko/luckperms/standalone/utils/TestSender.java @@ -0,0 +1,113 @@ +/* + * 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.utils; + +import me.lucko.luckperms.standalone.app.integration.StandaloneSender; +import me.lucko.luckperms.standalone.app.integration.StandaloneUser; +import net.kyori.adventure.text.Component; +import net.luckperms.api.util.Tristate; + +import java.util.Locale; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; +import java.util.function.Function; + +public class TestSender implements StandaloneSender { + + private final Set> messageSinks; + + private String name = "StandaloneUser"; + private UUID uniqueId = UUID.randomUUID(); + private boolean isConsole = false; + + private Function permissionChecker; + + public TestSender() { + this.messageSinks = new CopyOnWriteArraySet<>(); + this.messageSinks.add(StandaloneUser.INSTANCE::sendMessage); + } + + @Override + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public UUID getUniqueId() { + return this.uniqueId; + } + + public void setUniqueId(UUID uuid) { + this.uniqueId = uuid; + } + + @Override + public void sendMessage(Component component) { + for (Consumer sink : this.messageSinks) { + sink.accept(component); + } + } + + @Override + public Tristate getPermissionValue(String permission) { + return this.permissionChecker == null + ? Tristate.TRUE + : this.permissionChecker.apply(permission); + } + + @Override + public boolean hasPermission(String permission) { + return getPermissionValue(permission).asBoolean(); + } + + @Override + public boolean isConsole() { + return this.isConsole; + } + + public void setConsole(boolean console) { + this.isConsole = console; + } + + @Override + public Locale getLocale() { + return Locale.ENGLISH; + } + + public void setPermissionChecker(Function permissionChecker) { + this.permissionChecker = permissionChecker; + } + + public void addMessageSink(Consumer sink) { + this.messageSinks.add(sink); + } +} diff --git a/standalone/src/test/resources/example/hocon-combined/groups.conf b/standalone/src/test/resources/example/hocon-combined/groups.conf new file mode 100644 index 000000000..6cd167bde --- /dev/null +++ b/standalone/src/test/resources/example/hocon-combined/groups.conf @@ -0,0 +1,47 @@ +default { + permissions=[] +} +test { + meta=[ + { + key=foo + value=bar + } + ] + permissions=[ + { + context { + server=foo + test=test + world=bar + } + expiry=2532384000 + permission="group.default" + value=false + }, + { + context { + server=foo + test=test + world=bar + } + expiry=2532384000 + permission=test + value=false + }, + { + permission="example.permission" + value=true + } + ] + prefixes=[ + { + context { + server=foo + world=bar + } + prefix=TEST + priority=100 + } + ] +} diff --git a/standalone/src/test/resources/example/hocon-combined/tracks.conf b/standalone/src/test/resources/example/hocon-combined/tracks.conf new file mode 100644 index 000000000..f8018594f --- /dev/null +++ b/standalone/src/test/resources/example/hocon-combined/tracks.conf @@ -0,0 +1,6 @@ +example { + groups=[ + default, + test + ] +} diff --git a/standalone/src/test/resources/example/hocon-combined/users.conf b/standalone/src/test/resources/example/hocon-combined/users.conf new file mode 100644 index 000000000..9b23671e9 --- /dev/null +++ b/standalone/src/test/resources/example/hocon-combined/users.conf @@ -0,0 +1,51 @@ +c1d60c50-70b5-4722-8057-87767557e50d { + meta=[ + { + key=foo + value=bar + } + ] + name=Luck + parents=[ + { + group=default + } + ] + permissions=[ + { + context { + server=foo + test=test + world=bar + } + expiry=2532384000 + permission="group.default" + value=false + }, + { + context { + server=foo + test=test + world=bar + } + expiry=2532384000 + permission=test + value=false + }, + { + permission="example.permission" + value=true + } + ] + prefixes=[ + { + context { + server=foo + world=bar + } + prefix=TEST + priority=100 + } + ] + primary-group=default +} diff --git a/standalone/src/test/resources/example/hocon/groups/default.conf b/standalone/src/test/resources/example/hocon/groups/default.conf new file mode 100644 index 000000000..54a5379f8 --- /dev/null +++ b/standalone/src/test/resources/example/hocon/groups/default.conf @@ -0,0 +1 @@ +name=default diff --git a/standalone/src/test/resources/example/hocon/groups/test.conf b/standalone/src/test/resources/example/hocon/groups/test.conf new file mode 100644 index 000000000..ed27b9332 --- /dev/null +++ b/standalone/src/test/resources/example/hocon/groups/test.conf @@ -0,0 +1,43 @@ +meta=[ + { + key=foo + value=bar + } +] +name=test +permissions=[ + { + context { + server=foo + test=test + world=bar + } + expiry=2532384000 + permission="group.default" + value=false + }, + { + context { + server=foo + test=test + world=bar + } + expiry=2532384000 + permission=test + value=false + }, + { + permission="example.permission" + value=true + } +] +prefixes=[ + { + context { + server=foo + world=bar + } + prefix=TEST + priority=100 + } +] 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/hocon/users/c1d60c50-70b5-4722-8057-87767557e50d.conf b/standalone/src/test/resources/example/hocon/users/c1d60c50-70b5-4722-8057-87767557e50d.conf new file mode 100644 index 000000000..3801deb53 --- /dev/null +++ b/standalone/src/test/resources/example/hocon/users/c1d60c50-70b5-4722-8057-87767557e50d.conf @@ -0,0 +1,50 @@ +meta=[ + { + key=foo + value=bar + } +] +name=Luck +parents=[ + { + group=default + } +] +permissions=[ + { + context { + server=foo + test=test + world=bar + } + expiry=2532384000 + permission="group.default" + value=false + }, + { + context { + server=foo + test=test + world=bar + } + expiry=2532384000 + permission=test + value=false + }, + { + permission="example.permission" + value=true + } +] +prefixes=[ + { + context { + server=foo + world=bar + } + prefix=TEST + priority=100 + } +] +primary-group=default +uuid=c1d60c50-70b5-4722-8057-87767557e50d diff --git a/standalone/src/test/resources/example/json-combined/groups.json b/standalone/src/test/resources/example/json-combined/groups.json new file mode 100644 index 000000000..8d463416a --- /dev/null +++ b/standalone/src/test/resources/example/json-combined/groups.json @@ -0,0 +1,46 @@ +{ + "test": { + "permissions": [ + { + "permission": "group.default", + "value": false, + "expiry": 2532384000, + "context": { + "server": "foo", + "test": "test", + "world": "bar" + } + }, + { + "permission": "test", + "value": false, + "expiry": 2532384000, + "context": { + "server": "foo", + "test": "test", + "world": "bar" + } + }, + { + "permission": "example.permission", + "value": true + } + ], + "prefixes": [ + { + "prefix": "TEST", + "priority": 100, + "context": { + "server": "foo", + "world": "bar" + } + } + ], + "meta": [ + { + "key": "foo", + "value": "bar" + } + ] + } +} diff --git a/standalone/src/test/resources/example/json-combined/tracks.json b/standalone/src/test/resources/example/json-combined/tracks.json new file mode 100644 index 000000000..73b87b6fe --- /dev/null +++ b/standalone/src/test/resources/example/json-combined/tracks.json @@ -0,0 +1,8 @@ +{ + "example": { + "groups": [ + "default", + "test" + ] + } +} diff --git a/standalone/src/test/resources/example/json-combined/users.json b/standalone/src/test/resources/example/json-combined/users.json new file mode 100644 index 000000000..e35011724 --- /dev/null +++ b/standalone/src/test/resources/example/json-combined/users.json @@ -0,0 +1,53 @@ +{ + "c1d60c50-70b5-4722-8057-87767557e50d": { + "name": "Luck", + "primaryGroup": "default", + "permissions": [ + { + "permission": "group.default", + "value": false, + "expiry": 2532384000, + "context": { + "server": "foo", + "test": "test", + "world": "bar" + } + }, + { + "permission": "test", + "value": false, + "expiry": 2532384000, + "context": { + "server": "foo", + "test": "test", + "world": "bar" + } + }, + { + "permission": "example.permission", + "value": true + } + ], + "parents": [ + { + "group": "default" + } + ], + "prefixes": [ + { + "prefix": "TEST", + "priority": 100, + "context": { + "server": "foo", + "world": "bar" + } + } + ], + "meta": [ + { + "key": "foo", + "value": "bar" + } + ] + } +} diff --git a/standalone/src/test/resources/example/json/groups/default.json b/standalone/src/test/resources/example/json/groups/default.json new file mode 100644 index 000000000..b61c30f95 --- /dev/null +++ b/standalone/src/test/resources/example/json/groups/default.json @@ -0,0 +1,3 @@ +{ + "name": "default" +} diff --git a/standalone/src/test/resources/example/json/groups/test.json b/standalone/src/test/resources/example/json/groups/test.json new file mode 100644 index 000000000..8e96f3d5d --- /dev/null +++ b/standalone/src/test/resources/example/json/groups/test.json @@ -0,0 +1,45 @@ +{ + "name": "test", + "permissions": [ + { + "permission": "group.default", + "value": false, + "expiry": 2532384000, + "context": { + "server": "foo", + "test": "test", + "world": "bar" + } + }, + { + "permission": "test", + "value": false, + "expiry": 2532384000, + "context": { + "server": "foo", + "test": "test", + "world": "bar" + } + }, + { + "permission": "example.permission", + "value": true + } + ], + "prefixes": [ + { + "prefix": "TEST", + "priority": 100, + "context": { + "server": "foo", + "world": "bar" + } + } + ], + "meta": [ + { + "key": "foo", + "value": "bar" + } + ] +} 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/json/users/c1d60c50-70b5-4722-8057-87767557e50d.json b/standalone/src/test/resources/example/json/users/c1d60c50-70b5-4722-8057-87767557e50d.json new file mode 100644 index 000000000..8a5aa6e02 --- /dev/null +++ b/standalone/src/test/resources/example/json/users/c1d60c50-70b5-4722-8057-87767557e50d.json @@ -0,0 +1,52 @@ +{ + "uuid": "c1d60c50-70b5-4722-8057-87767557e50d", + "name": "Luck", + "primaryGroup": "default", + "permissions": [ + { + "permission": "group.default", + "value": false, + "expiry": 2532384000, + "context": { + "server": "foo", + "test": "test", + "world": "bar" + } + }, + { + "permission": "test", + "value": false, + "expiry": 2532384000, + "context": { + "server": "foo", + "test": "test", + "world": "bar" + } + }, + { + "permission": "example.permission", + "value": true + } + ], + "parents": [ + { + "group": "default" + } + ], + "prefixes": [ + { + "prefix": "TEST", + "priority": 100, + "context": { + "server": "foo", + "world": "bar" + } + } + ], + "meta": [ + { + "key": "foo", + "value": "bar" + } + ] +} diff --git a/standalone/src/test/resources/example/yaml-combined/groups.yml b/standalone/src/test/resources/example/yaml-combined/groups.yml new file mode 100644 index 000000000..97c2610ea --- /dev/null +++ b/standalone/src/test/resources/example/yaml-combined/groups.yml @@ -0,0 +1,28 @@ +default: + permissions: [] +test: + permissions: + - group.default: + value: false + expiry: 2532384000 + context: + server: foo + test: test + world: bar + - test: + value: false + expiry: 2532384000 + context: + server: foo + test: test + world: bar + - example.permission + prefixes: + - TEST: + priority: 100 + context: + server: foo + world: bar + meta: + - 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 new file mode 100644 index 000000000..5482d7ad9 --- /dev/null +++ b/standalone/src/test/resources/example/yaml-combined/tracks.yml @@ -0,0 +1,4 @@ +example: + groups: + - default + - test diff --git a/standalone/src/test/resources/example/yaml-combined/users.yml b/standalone/src/test/resources/example/yaml-combined/users.yml new file mode 100644 index 000000000..2421d4de1 --- /dev/null +++ b/standalone/src/test/resources/example/yaml-combined/users.yml @@ -0,0 +1,30 @@ +c1d60c50-70b5-4722-8057-87767557e50d: + name: Luck + primary-group: default + permissions: + - group.default: + value: false + expiry: 2532384000 + context: + server: foo + test: test + world: bar + - test: + value: false + expiry: 2532384000 + context: + server: foo + test: test + world: bar + - example.permission + parents: + - default + prefixes: + - TEST: + priority: 100 + context: + server: foo + world: bar + meta: + - foo: + value: bar diff --git a/standalone/src/test/resources/example/yaml/groups/default.yml b/standalone/src/test/resources/example/yaml/groups/default.yml new file mode 100644 index 000000000..970ba0c56 --- /dev/null +++ b/standalone/src/test/resources/example/yaml/groups/default.yml @@ -0,0 +1 @@ +name: default diff --git a/standalone/src/test/resources/example/yaml/groups/test.yml b/standalone/src/test/resources/example/yaml/groups/test.yml new file mode 100644 index 000000000..7ec1139c2 --- /dev/null +++ b/standalone/src/test/resources/example/yaml/groups/test.yml @@ -0,0 +1,26 @@ +name: test +permissions: +- group.default: + value: false + expiry: 2532384000 + context: + server: foo + test: test + world: bar +- test: + value: false + expiry: 2532384000 + context: + server: foo + test: test + world: bar +- example.permission +prefixes: +- TEST: + priority: 100 + context: + server: foo + world: bar +meta: +- foo: + value: bar 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 diff --git a/standalone/src/test/resources/example/yaml/users/c1d60c50-70b5-4722-8057-87767557e50d.yml b/standalone/src/test/resources/example/yaml/users/c1d60c50-70b5-4722-8057-87767557e50d.yml new file mode 100644 index 000000000..b00f6cae2 --- /dev/null +++ b/standalone/src/test/resources/example/yaml/users/c1d60c50-70b5-4722-8057-87767557e50d.yml @@ -0,0 +1,30 @@ +uuid: c1d60c50-70b5-4722-8057-87767557e50d +name: Luck +primary-group: default +permissions: +- group.default: + value: false + expiry: 2532384000 + context: + server: foo + test: test + world: bar +- test: + value: false + expiry: 2532384000 + context: + server: foo + test: test + world: bar +- example.permission +parents: +- default +prefixes: +- TEST: + priority: 100 + context: + server: foo + world: bar +meta: +- foo: + value: bar diff --git a/standalone/src/test/resources/log4j2.xml b/standalone/src/test/resources/log4j2.xml new file mode 100644 index 000000000..ffb070356 --- /dev/null +++ b/standalone/src/test/resources/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/velocity/build.gradle b/velocity/build.gradle index 9ea150c14..370f115ef 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -1,26 +1,20 @@ plugins { - id 'net.kyori.blossom' version '1.1.0' - id 'com.github.johnrengelman.shadow' version '2.0.4' + alias(libs.plugins.shadow) } repositories { - maven { url 'https://repo.velocitypowered.com/snapshots/' } + maven { url 'https://repo.papermc.io/repository/maven-public/' } } dependencies { - compile project(':common') + implementation project(':common') - compileOnly 'com.velocitypowered:velocity-api:1.1.0-SNAPSHOT' - annotationProcessor 'com.velocitypowered:velocity-api:1.1.0-SNAPSHOT' -} - -blossom { - replaceTokenIn('src/main/java/me/lucko/luckperms/velocity/LPVelocityBootstrap.java') - replaceToken '@version@', project.ext.fullVersion + compileOnly 'com.velocitypowered:velocity-api:3.1.1' + annotationProcessor 'com.velocitypowered:velocity-api:3.1.1' } shadowJar { - archiveName = "LuckPerms-Velocity-${project.ext.fullVersion}.jar" + archiveFileName = "LuckPerms-Velocity-${project.ext.fullVersion}.jar" dependencies { include(dependency('net.luckperms:.*')) @@ -41,6 +35,7 @@ shadowJar { relocate 'com.mongodb', 'me.lucko.luckperms.lib.mongodb' relocate 'org.bson', 'me.lucko.luckperms.lib.bson' relocate 'redis.clients.jedis', 'me.lucko.luckperms.lib.jedis' + relocate 'io.nats.client', 'me.lucko.luckperms.lib.nats' relocate 'com.rabbitmq', 'me.lucko.luckperms.lib.rabbitmq' relocate 'org.apache.commons.pool2', 'me.lucko.luckperms.lib.commonspool2' relocate 'ninja.leaping.configurate', 'me.lucko.luckperms.lib.configurate' diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/LPVelocityBootstrap.java b/velocity/src/main/java/me/lucko/luckperms/velocity/LPVelocityBootstrap.java index 6dc3a9209..b3cf61672 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/LPVelocityBootstrap.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/LPVelocityBootstrap.java @@ -34,15 +34,13 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; - import me.lucko.luckperms.common.plugin.bootstrap.LuckPermsBootstrap; import me.lucko.luckperms.common.plugin.classpath.ClassPathAppender; import me.lucko.luckperms.common.plugin.logging.PluginLogger; import me.lucko.luckperms.common.plugin.logging.Slf4jPluginLogger; import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; - +import me.lucko.luckperms.common.util.BuildInfo; import net.luckperms.api.platform.Platform; - import org.slf4j.Logger; import java.nio.file.Path; @@ -60,7 +58,7 @@ @Plugin( id = "luckperms", name = "LuckPerms", - version = "@version@", + version = BuildInfo.VERSION, authors = "Luck", description = "A permissions plugin", url = "https://luckperms.net" @@ -171,7 +169,7 @@ public ProxyServer getProxy() { @Override public String getVersion() { - return "@version@"; + return BuildInfo.VERSION; } @Override diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/LPVelocityPlugin.java b/velocity/src/main/java/me/lucko/luckperms/velocity/LPVelocityPlugin.java index b3c5186f0..a8430298c 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/LPVelocityPlugin.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/LPVelocityPlugin.java @@ -40,22 +40,18 @@ import me.lucko.luckperms.common.plugin.AbstractLuckPermsPlugin; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.tasks.CacheHousekeepingTask; -import me.lucko.luckperms.common.tasks.ExpireTemporaryTask; import me.lucko.luckperms.velocity.calculator.VelocityCalculatorFactory; import me.lucko.luckperms.velocity.context.VelocityContextManager; import me.lucko.luckperms.velocity.context.VelocityPlayerCalculator; import me.lucko.luckperms.velocity.listeners.MonitoringPermissionCheckListener; import me.lucko.luckperms.velocity.listeners.VelocityConnectionListener; import me.lucko.luckperms.velocity.messaging.VelocityMessagingFactory; - import net.luckperms.api.LuckPerms; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.query.QueryOptions; import java.util.Optional; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.stream.Stream; /** @@ -158,12 +154,6 @@ protected void registerApiOnPlatform(LuckPerms api) { // Velocity doesn't have a services manager } - @Override - protected void registerHousekeepingTasks() { - this.bootstrap.getScheduler().asyncRepeating(new ExpireTemporaryTask(this), 3, TimeUnit.SECONDS); - this.bootstrap.getScheduler().asyncRepeating(new CacheHousekeepingTask(this), 2, TimeUnit.MINUTES); - } - @Override protected void performFinalSetup() { diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityCommandExecutor.java b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityCommandExecutor.java index 7f93f3b06..560077586 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityCommandExecutor.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityCommandExecutor.java @@ -28,7 +28,6 @@ import com.velocitypowered.api.command.RawCommand; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.ProxyServer; - import me.lucko.luckperms.common.command.CommandManager; import me.lucko.luckperms.common.command.utils.ArgumentTokenizer; import me.lucko.luckperms.common.sender.Sender; diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityConfigAdapter.java b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityConfigAdapter.java index 9e21cecd4..a5df89716 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityConfigAdapter.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityConfigAdapter.java @@ -28,7 +28,6 @@ import me.lucko.luckperms.common.config.generic.adapter.ConfigurateConfigAdapter; import me.lucko.luckperms.common.config.generic.adapter.ConfigurationAdapter; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; - import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.loader.ConfigurationLoader; import ninja.leaping.configurate.yaml.YAMLConfigurationLoader; diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityEventBus.java b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityEventBus.java index 2393a41d3..14def5829 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityEventBus.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocityEventBus.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.velocity; import com.velocitypowered.api.plugin.PluginContainer; - import me.lucko.luckperms.common.api.LuckPermsApiProvider; import me.lucko.luckperms.common.event.AbstractEventBus; diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocitySchedulerAdapter.java b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocitySchedulerAdapter.java index 33e458240..3c070f816 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocitySchedulerAdapter.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocitySchedulerAdapter.java @@ -26,7 +26,6 @@ package me.lucko.luckperms.velocity; import com.velocitypowered.api.scheduler.ScheduledTask; - import me.lucko.luckperms.common.plugin.scheduler.SchedulerAdapter; import me.lucko.luckperms.common.plugin.scheduler.SchedulerTask; import me.lucko.luckperms.common.util.Iterators; @@ -53,11 +52,6 @@ public Executor async() { return this.executor; } - @Override - public Executor sync() { - return this.executor; - } - @Override public SchedulerTask asyncLater(Runnable task, long delay, TimeUnit unit) { ScheduledTask t = this.bootstrap.getProxy().getScheduler().buildTask(this.bootstrap, task) diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocitySenderFactory.java b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocitySenderFactory.java index 1728ec885..61e3b3ab4 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/VelocitySenderFactory.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/VelocitySenderFactory.java @@ -28,13 +28,11 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.Player; - 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.velocity.service.CompatibilityUtil; import me.lucko.luckperms.velocity.util.AdventureCompat; - import net.kyori.adventure.text.Component; import net.luckperms.api.util.Tristate; diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/calculator/VelocityCalculatorFactory.java b/velocity/src/main/java/me/lucko/luckperms/velocity/calculator/VelocityCalculatorFactory.java index 694eebf39..8d9e1f13f 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/calculator/VelocityCalculatorFactory.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/calculator/VelocityCalculatorFactory.java @@ -28,6 +28,7 @@ import me.lucko.luckperms.common.cacheddata.CacheMetadata; import me.lucko.luckperms.common.calculator.CalculatorFactory; import me.lucko.luckperms.common.calculator.PermissionCalculator; +import me.lucko.luckperms.common.calculator.PermissionCalculatorMonitored; import me.lucko.luckperms.common.calculator.processor.DirectProcessor; import me.lucko.luckperms.common.calculator.processor.PermissionProcessor; import me.lucko.luckperms.common.calculator.processor.RegexProcessor; @@ -35,11 +36,12 @@ import me.lucko.luckperms.common.calculator.processor.WildcardProcessor; import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.velocity.LPVelocityPlugin; - +import net.luckperms.api.node.Node; import net.luckperms.api.query.QueryOptions; import java.util.ArrayList; import java.util.List; +import java.util.Map; public class VelocityCalculatorFactory implements CalculatorFactory { private final LPVelocityPlugin plugin; @@ -49,23 +51,23 @@ public VelocityCalculatorFactory(LPVelocityPlugin plugin) { } @Override - public PermissionCalculator build(QueryOptions queryOptions, CacheMetadata metadata) { + public PermissionCalculator build(QueryOptions queryOptions, Map sourceMap, CacheMetadata metadata) { List processors = new ArrayList<>(4); - processors.add(new DirectProcessor()); + processors.add(new DirectProcessor(sourceMap)); if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_REGEX)) { - processors.add(new RegexProcessor()); + processors.add(new RegexProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS)) { - processors.add(new WildcardProcessor()); + processors.add(new WildcardProcessor(sourceMap)); } if (this.plugin.getConfiguration().get(ConfigKeys.APPLYING_WILDCARDS_SPONGE)) { - processors.add(new SpongeWildcardProcessor()); + processors.add(new SpongeWildcardProcessor(sourceMap)); } - return new PermissionCalculator(this.plugin, metadata, processors); + return new PermissionCalculatorMonitored(this.plugin, metadata, processors); } } diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/context/VelocityContextManager.java b/velocity/src/main/java/me/lucko/luckperms/velocity/context/VelocityContextManager.java index ee76620ff..da7412542 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/context/VelocityContextManager.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/context/VelocityContextManager.java @@ -25,27 +25,13 @@ package me.lucko.luckperms.velocity.context; -import com.github.benmanes.caffeine.cache.LoadingCache; import com.velocitypowered.api.proxy.Player; - -import me.lucko.luckperms.common.context.ContextManager; -import me.lucko.luckperms.common.context.QueryOptionsCache; -import me.lucko.luckperms.common.context.QueryOptionsSupplier; -import me.lucko.luckperms.common.util.CaffeineFactory; +import me.lucko.luckperms.common.context.manager.SimpleContextManager; import me.lucko.luckperms.velocity.LPVelocityPlugin; -import net.luckperms.api.context.ImmutableContextSet; -import net.luckperms.api.query.QueryOptions; - import java.util.UUID; -import java.util.concurrent.TimeUnit; - -public class VelocityContextManager extends ContextManager { - - private final LoadingCache> subjectCaches = CaffeineFactory.newBuilder() - .expireAfterAccess(1, TimeUnit.MINUTES) - .build(key -> new QueryOptionsCache<>(key, this)); +public class VelocityContextManager extends SimpleContextManager { public VelocityContextManager(LPVelocityPlugin plugin) { super(plugin, Player.class, Player.class); } @@ -54,26 +40,4 @@ public VelocityContextManager(LPVelocityPlugin plugin) { public UUID getUniqueId(Player player) { return player.getUniqueId(); } - - @Override - public QueryOptionsSupplier getCacheFor(Player subject) { - if (subject == null) { - throw new NullPointerException("subject"); - } - - return this.subjectCaches.get(subject); - } - - @Override - protected void invalidateCache(Player subject) { - QueryOptionsCache cache = this.subjectCaches.getIfPresent(subject); - if (cache != null) { - cache.invalidate(); - } - } - - @Override - public QueryOptions formQueryOptions(Player subject, ImmutableContextSet contextSet) { - return formQueryOptions(contextSet); - } } diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/context/VelocityPlayerCalculator.java b/velocity/src/main/java/me/lucko/luckperms/velocity/context/VelocityPlayerCalculator.java index 6aa0d57f6..55b1f4297 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/context/VelocityPlayerCalculator.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/context/VelocityPlayerCalculator.java @@ -31,17 +31,14 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; - import me.lucko.luckperms.common.config.ConfigKeys; -import me.lucko.luckperms.common.context.contextset.ImmutableContextSetImpl; +import me.lucko.luckperms.common.context.ImmutableContextSetImpl; import me.lucko.luckperms.velocity.LPVelocityPlugin; - import net.luckperms.api.context.ContextCalculator; import net.luckperms.api.context.ContextConsumer; import net.luckperms.api.context.ContextSet; import net.luckperms.api.context.DefaultContextKeys; import net.luckperms.api.context.ImmutableContextSet; - import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.NotNull; diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/listeners/MonitoringPermissionCheckListener.java b/velocity/src/main/java/me/lucko/luckperms/velocity/listeners/MonitoringPermissionCheckListener.java index 81db28ec4..a559cadfb 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/listeners/MonitoringPermissionCheckListener.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/listeners/MonitoringPermissionCheckListener.java @@ -32,16 +32,13 @@ import com.velocitypowered.api.permission.PermissionProvider; import com.velocitypowered.api.permission.PermissionSubject; import com.velocitypowered.api.proxy.Player; - -import me.lucko.luckperms.common.calculator.result.TristateResult; +import me.lucko.luckperms.common.cacheddata.result.TristateResult; import me.lucko.luckperms.common.query.QueryOptionsImpl; import me.lucko.luckperms.common.verbose.VerboseCheckTarget; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; +import me.lucko.luckperms.common.verbose.event.CheckOrigin; import me.lucko.luckperms.velocity.LPVelocityPlugin; import me.lucko.luckperms.velocity.service.CompatibilityUtil; - import net.luckperms.api.util.Tristate; - import org.checkerframework.checker.nullness.qual.NonNull; public class MonitoringPermissionCheckListener { @@ -91,7 +88,7 @@ private final class MonitoredPermissionFunction implements PermissionFunction { // report result Tristate result = CompatibilityUtil.convertTristate(setting); - MonitoringPermissionCheckListener.this.plugin.getVerboseHandler().offerPermissionCheckEvent(PermissionCheckEvent.Origin.PLATFORM_LOOKUP_CHECK, this.verboseCheckTarget, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.of(result)); + MonitoringPermissionCheckListener.this.plugin.getVerboseHandler().offerPermissionCheckEvent(CheckOrigin.PLATFORM_API_HAS_PERMISSION_SET, this.verboseCheckTarget, QueryOptionsImpl.DEFAULT_CONTEXTUAL, permission, TristateResult.forMonitoredResult(result)); MonitoringPermissionCheckListener.this.plugin.getPermissionRegistry().offer(permission); return setting; diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/listeners/VelocityConnectionListener.java b/velocity/src/main/java/me/lucko/luckperms/velocity/listeners/VelocityConnectionListener.java index eed927954..98d7c5ead 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/listeners/VelocityConnectionListener.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/listeners/VelocityConnectionListener.java @@ -25,19 +25,20 @@ package me.lucko.luckperms.velocity.listeners; +import com.velocitypowered.api.event.Continuation; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.event.permission.PermissionsSetupEvent; import com.velocitypowered.api.proxy.Player; - import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.locale.TranslationManager; import me.lucko.luckperms.common.model.User; import me.lucko.luckperms.common.plugin.util.AbstractConnectionListener; import me.lucko.luckperms.velocity.LPVelocityPlugin; +import me.lucko.luckperms.velocity.context.VelocityContextManager; import me.lucko.luckperms.velocity.service.PlayerPermissionProvider; import me.lucko.luckperms.velocity.util.AdventureCompat; @@ -58,7 +59,7 @@ public VelocityConnectionListener(LPVelocityPlugin plugin) { } @Subscribe - public void onPlayerPermissionsSetup(PermissionsSetupEvent e) { + public void onPlayerPermissionsSetup(PermissionsSetupEvent e, Continuation continuation) { /* Called when the player first attempts a connection with the server. The PermissionsSetupEvent is called for players just before the Login event @@ -67,6 +68,7 @@ public void onPlayerPermissionsSetup(PermissionsSetupEvent e) { Offline mode. */ if (!(e.getSubject() instanceof Player)) { + continuation.resume(); return; } @@ -76,30 +78,35 @@ public void onPlayerPermissionsSetup(PermissionsSetupEvent e) { this.plugin.getLogger().info("Processing pre-login for " + p.getUniqueId() + " - " + p.getUsername()); } - /* Actually process the login for the connection. - We do this here to delay the login until the data is ready. - If the login gets cancelled later on, then this will be cleaned up. - - This includes: - - loading uuid data - - loading permissions - - creating a user instance in the UserManager for this connection. - - setting up cached data. */ - try { - User user = loadUser(p.getUniqueId(), p.getUsername()); - recordConnection(p.getUniqueId()); - e.setProvider(new PlayerPermissionProvider(p, user, this.plugin.getContextManager().getCacheFor(p))); - this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(p.getUniqueId(), p.getUsername(), user); - } catch (Exception ex) { - this.plugin.getLogger().severe("Exception occurred whilst loading data for " + p.getUniqueId() + " - " + p.getUsername(), ex); - - // there was some error loading - if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { - // cancel the login attempt - this.deniedLogin.add(p.getUniqueId()); + this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + /* Actually process the login for the connection. + We do this here to delay the login until the data is ready. + If the login gets cancelled later on, then this will be cleaned up. + + This includes: + - loading uuid data + - loading permissions + - creating a user instance in the UserManager for this connection. + - setting up cached data. */ + try { + User user = loadUser(p.getUniqueId(), p.getUsername()); + recordConnection(p.getUniqueId()); + VelocityContextManager contextManager = this.plugin.getContextManager(); + e.setProvider(new PlayerPermissionProvider(p, user, contextManager::getQueryOptions)); + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(p.getUniqueId(), p.getUsername(), user); + } catch (Exception ex) { + this.plugin.getLogger().severe("Exception occurred whilst loading data for " + p.getUniqueId() + " - " + p.getUsername(), ex); + + // there was some error loading + if (this.plugin.getConfiguration().get(ConfigKeys.CANCEL_FAILED_LOGINS)) { + // cancel the login attempt + this.deniedLogin.add(p.getUniqueId()); + } + this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(p.getUniqueId(), p.getUsername(), null); + } finally { + continuation.resume(); } - this.plugin.getEventDispatcher().dispatchPlayerLoginProcess(p.getUniqueId(), p.getUsername(), null); - } + }); } @Subscribe(order = PostOrder.FIRST) diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/messaging/PluginMessageMessenger.java b/velocity/src/main/java/me/lucko/luckperms/velocity/messaging/PluginMessageMessenger.java index df3e58987..acc76755d 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/messaging/PluginMessageMessenger.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/messaging/PluginMessageMessenger.java @@ -25,9 +25,6 @@ package me.lucko.luckperms.velocity.messaging; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent.ForwardResult; @@ -36,27 +33,22 @@ import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.proxy.server.RegisteredServer; - +import me.lucko.luckperms.common.messaging.pluginmsg.AbstractPluginMessageMessenger; import me.lucko.luckperms.velocity.LPVelocityPlugin; - import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; -import net.luckperms.api.messenger.message.OutgoingMessage; - -import org.checkerframework.checker.nullness.qual.NonNull; /** * An implementation of {@link Messenger} using the plugin messaging channels. */ -public class PluginMessageMessenger implements Messenger { - private static final ChannelIdentifier CHANNEL = MinecraftChannelIdentifier.create("luckperms", "update"); +public class PluginMessageMessenger extends AbstractPluginMessageMessenger { + private static final ChannelIdentifier CHANNEL = MinecraftChannelIdentifier.from(AbstractPluginMessageMessenger.CHANNEL); private final LPVelocityPlugin plugin; - private final IncomingMessageConsumer consumer; public PluginMessageMessenger(LPVelocityPlugin plugin, IncomingMessageConsumer consumer) { + super(consumer); this.plugin = plugin; - this.consumer = consumer; } public void init() { @@ -72,21 +64,13 @@ public void close() { proxy.getEventManager().unregisterListener(this.plugin.getBootstrap(), this); } - private void dispatchMessage(byte[] message) { + @Override + protected void sendOutgoingMessage(byte[] buf) { for (RegisteredServer server : this.plugin.getBootstrap().getProxy().getAllServers()) { - server.sendPluginMessage(CHANNEL, message); + server.sendPluginMessage(CHANNEL, buf); } } - @Override - public void sendOutgoingMessage(@NonNull OutgoingMessage outgoingMessage) { - ByteArrayDataOutput out = ByteStreams.newDataOutput(); - out.writeUTF(outgoingMessage.asEncodedString()); - - byte[] message = out.toByteArray(); - dispatchMessage(message); - } - @Subscribe public void onPluginMessage(PluginMessageEvent e) { // compare the underlying text representation of the channel @@ -102,12 +86,11 @@ public void onPluginMessage(PluginMessageEvent e) { return; } - ByteArrayDataInput in = e.dataAsDataStream(); - String msg = in.readUTF(); + byte[] buf = e.getData(); - if (this.consumer.consumeIncomingMessageAsString(msg)) { + if (handleIncomingMessage(buf)) { // Forward to other servers - this.plugin.getBootstrap().getScheduler().executeAsync(() -> dispatchMessage(e.getData())); + this.plugin.getBootstrap().getScheduler().executeAsync(() -> sendOutgoingMessage(buf)); } } } diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/messaging/VelocityMessagingFactory.java b/velocity/src/main/java/me/lucko/luckperms/velocity/messaging/VelocityMessagingFactory.java index 33c6b21d8..bdd700047 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/messaging/VelocityMessagingFactory.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/messaging/VelocityMessagingFactory.java @@ -29,11 +29,9 @@ import me.lucko.luckperms.common.messaging.LuckPermsMessagingService; import me.lucko.luckperms.common.messaging.MessagingFactory; import me.lucko.luckperms.velocity.LPVelocityPlugin; - import net.luckperms.api.messenger.IncomingMessageConsumer; import net.luckperms.api.messenger.Messenger; import net.luckperms.api.messenger.MessengerProvider; - import org.checkerframework.checker.nullness.qual.NonNull; public class VelocityMessagingFactory extends MessagingFactory { diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/service/PlayerPermissionProvider.java b/velocity/src/main/java/me/lucko/luckperms/velocity/service/PlayerPermissionProvider.java index a3ca824cd..3e967d3d2 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/service/PlayerPermissionProvider.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/service/PlayerPermissionProvider.java @@ -25,25 +25,24 @@ package me.lucko.luckperms.velocity.service; +import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.velocitypowered.api.permission.PermissionFunction; import com.velocitypowered.api.permission.PermissionProvider; import com.velocitypowered.api.permission.PermissionSubject; import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.Player; - -import me.lucko.luckperms.common.context.QueryOptionsSupplier; import me.lucko.luckperms.common.model.User; -import me.lucko.luckperms.common.verbose.event.PermissionCheckEvent; - +import me.lucko.luckperms.common.verbose.event.CheckOrigin; +import net.luckperms.api.query.QueryOptions; import org.checkerframework.checker.nullness.qual.NonNull; public class PlayerPermissionProvider implements PermissionProvider, PermissionFunction { private final Player player; private final User user; - private final QueryOptionsSupplier queryOptionsSupplier; + private final Function queryOptionsSupplier; - public PlayerPermissionProvider(Player player, User user, QueryOptionsSupplier queryOptionsSupplier) { + public PlayerPermissionProvider(Player player, User user, Function queryOptionsSupplier) { this.player = player; this.user = user; this.queryOptionsSupplier = queryOptionsSupplier; @@ -57,6 +56,7 @@ public PlayerPermissionProvider(Player player, User user, QueryOptionsSupplier q @Override public @NonNull Tristate getPermissionValue(@NonNull String permission) { - return CompatibilityUtil.convertTristate(this.user.getCachedData().getPermissionData(this.queryOptionsSupplier.getQueryOptions()).checkPermission(permission, PermissionCheckEvent.Origin.PLATFORM_PERMISSION_CHECK).result()); + QueryOptions queryOptions = this.queryOptionsSupplier.apply(this.player); + return CompatibilityUtil.convertTristate(this.user.getCachedData().getPermissionData(queryOptions).checkPermission(permission, CheckOrigin.PLATFORM_API_HAS_PERMISSION).result()); } } diff --git a/velocity/src/main/java/me/lucko/luckperms/velocity/util/AdventureCompat.java b/velocity/src/main/java/me/lucko/luckperms/velocity/util/AdventureCompat.java index 1bda0c98c..ffc1df8fc 100644 --- a/velocity/src/main/java/me/lucko/luckperms/velocity/util/AdventureCompat.java +++ b/velocity/src/main/java/me/lucko/luckperms/velocity/util/AdventureCompat.java @@ -25,9 +25,9 @@ package me.lucko.luckperms.velocity.util; +import com.google.gson.JsonElement; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.ResultedEvent.ComponentResult; - import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -55,7 +55,7 @@ private AdventureCompat() {} Class componentClass = Class.forName(adventurePkg + "text.Component"); Class serializerClass = Class.forName(adventurePkg + "text.serializer.gson.GsonComponentSerializer"); - PLATFORM_SERIALIZER_DESERIALIZE = serializerClass.getMethod("deserialize", Object.class); + PLATFORM_SERIALIZER_DESERIALIZE = serializerClass.getMethod("deserializeFromTree", JsonElement.class); PLATFORM_SEND_MESSAGE = audienceClass.getMethod("sendMessage", componentClass); PLATFORM_COMPONENT_RESULT_DENIED = ComponentResult.class.getMethod("denied", componentClass); PLATFORM_SERIALIZER_INSTANCE = serializerClass.getMethod("gson").invoke(null); @@ -65,7 +65,7 @@ private AdventureCompat() {} } public static Object toPlatformComponent(Component component) { - String json = GsonComponentSerializer.gson().serialize(component); + JsonElement json = GsonComponentSerializer.gson().serializeToTree(component); try { return PLATFORM_SERIALIZER_DESERIALIZE.invoke(PLATFORM_SERIALIZER_INSTANCE, json); } catch (ReflectiveOperationException e) { diff --git a/velocity/src/main/resources/config.yml b/velocity/src/main/resources/config.yml index 382476b8d..8fba524ef 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 | # @@ -144,15 +144,25 @@ data: #verifyServerCertificate: false # The prefix for all LuckPerms SQL tables. + # + # - This only applies for remote SQL storage types (MySQL, MariaDB, etc). # - Change this if you want to use different tables for different servers. table-prefix: 'luckperms_' - # The prefix to use for all LuckPerms collections. Change this if you want to use different - # collections for different servers. The default is no prefix. + # The prefix to use for all LuckPerms MongoDB collections. + # + # - This only applies for the MongoDB storage type. + # - Change this if you want to use different collections for different servers. The default is no + # prefix. mongodb-collection-prefix: '' - # MongoDB ClientConnectionURI for use with replica sets and custom connection options - # - See https://docs.mongodb.com/manual/reference/connection-string/ + # The connection string URI to use to connect to the MongoDB instance. + # + # - When configured, this setting will override anything defined in the address, database, + # username or password fields above. + # - If you have a connection string that starts with 'mongodb://' or 'mongodb+srv://', enter it + # below. + # - For more information, please see https://docs.mongodb.com/manual/reference/connection-string/ mongodb-connection-uri: '' # Define settings for a "split" storage setup. @@ -220,6 +230,9 @@ watch-files: true # configured below. # => rabbitmq Uses RabbitMQ pub-sub to push changes. Your server connection info must be # configured below. +# => nats Uses Nats pub-sub to push changes. Your server connection info must be +# configured below. +# => custom Uses a messaging service provided using the LuckPerms API. # => auto Attempts to automatically setup a messaging service using redis or sql. messaging-service: auto @@ -238,10 +251,31 @@ 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 + username: '' password: '' + # Settings for Redis Sentinel. + # Sentinel provides high availability for Redis by monitoring master/replica instances. + # Port 26379 is used by default for sentinel nodes. + sentinel: + enabled: false + master: mymaster + addresses: + - localhost:26379 + username: '' + password: '' + +# Settings for Nats. +# Port 4222 is used by default; set address to "host:port" if differs +nats: + enabled: false + address: localhost + username: '' + password: '' + token: '' # Settings for RabbitMQ. # Port 5672 is used by default; set address to "host:port" if differs @@ -506,6 +540,13 @@ apply-shorthand: true # | Extra settings | # # +----------------------------------------------------------------------------------------------+ # +# A list of context calculators which will be skipped when calculating contexts. +# +# - You can disable context calculators by either: +# => specifying the Java class name used by the calculator (e.g. com.example.ExampleCalculator) +# => specifying a sub-section of the Java package used by the calculator (e.g. com.example) +disabled-context-calculators: [] + # Allows you to set "aliases" for the worlds sent forward for context calculation. # # - These aliases are provided in addition to the real world name. Applied recursively. @@ -557,7 +598,43 @@ allow-invalid-usernames: false # - If automation is needed, users should prefer using the LuckPerms API. skip-bulkupdate-confirmation: false +# If LuckPerms should prevent bulkupdate operations. +# +# - When set to true, bulkupdate operations (the /lp bulkupdate command) will not work. +# - When set to false, bulkupdate operations will be allowed via the console. +disable-bulkupdate: false + # If LuckPerms should allow a users primary group to be removed with the 'parent remove' command. # # - When this happens, the plugin will set their primary group back to default. prevent-primary-group-removal: false + +# If the plugin should run in "read-only" mode for commands. +# +# In this mode, players or the console will only be able to execute commands that read or view +# data, and will not be able to execute any LP commands that modify data. +# +# If enabling read-only mode for just players, be aware that some other plugins may allow players +# to execute commands as the console, allowing them to run LP commands indirectly. +# +# Note: This does not affect interactions with LuckPerms via the API. +commands-read-only-mode: + players: false + console: false + +# If LuckPerms commands should be disabled. When true, this will prevent all LP commands from being +# executed. In a sense this is a more extreme version of read-only mode (see above). +# +# LuckPerms will still act as the permission manager, but server administrators will be unable to +# make changes via commands. +# +# If disabling commands just for players, be aware that some other plugins may allow players to +# execute commands as the console, allowing them to run commands indirectly. +# +# If commands are disabled for both players and the console, LuckPerms will not attempt to register +# a command with the server at all & in a sense will be invisible to both players and admins. +# +# Note: This does not affect interactions with LuckPerms via the API. +disable-luckperms-commands: + players: false + console: false