diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 437e0da..bda91df 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -# Builds a Java project with Maven +# Builds a Java project with Gradle name: Build on: @@ -20,7 +20,9 @@ jobs: with: java-version: '17' distribution: 'temurin' - cache: maven - - name: Build with Maven - run: mvn -B package + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build with Gradle + run: ./gradlew build diff --git a/.gitignore b/.gitignore index 2df6176..dfac3e3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,9 @@ out/ # Maven target/ +# Gradle +.gradle/ +build/ + # Common working directory run/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..411e2a6 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + `java-library` + // Shade plugin + id("com.gradleup.shadow") version "9.3.0" +} + +repositories { + mavenCentral() + // Spigot + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + // Jitpack + maven("https://jitpack.io") + // Floodgate / Geyser + maven("https://repo.opencollab.dev/main/") +} + +dependencies { + implementation(libs.dev.dejvokep.boosted.yaml) + implementation(libs.com.github.earthcow.javadiscordwebhook) + implementation(libs.org.bstats.bstats.api) + + compileOnly(files("libs/ThemisAPI.jar")) + compileOnly(libs.org.spigotmc.spigot.api) + compileOnly(libs.org.jetbrains.annotations) + compileOnly(libs.org.geysermc.floodgate.api) +} + +group = "xyz.earthcow" +version = "0.3.1" + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.withType() { + options.encoding = "UTF-8" +} + +tasks.processResources { + val props = mapOf( + "name" to project.name, + "version" to project.version, + ) + + inputs.properties(props) + + filesMatching("plugin.yml") { + expand(props) + } +} + +tasks.shadowJar { + // Overwrite default jar + archiveClassifier.set("") + // Relocate shaded dependencies to internal libs directory + relocate("dev.dejvokep.boostedyaml", "xyz.earthcow.themistodiscord.libs.boostedyaml") + relocate("org.bstats", "xyz.earthcow.themistodiscord.libs.bstats") + // Exclude annotation packages from the uber jar file + exclude("org/intellij/**", "org/jetbrains/**") +} + +tasks.build { + dependsOn(tasks.shadowJar) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..5ad6974 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.configuration-cache=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..83e006d --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,18 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +com-github-earthcow-javadiscordwebhook = "v1.3.0" +dev-dejvokep-boosted-yaml = "1.3.7" +org-geysermc-floodgate-api = "2.2.4-SNAPSHOT" +org-jetbrains-annotations = "24.1.0" +org-spigotmc-spigot-api = "1.17-R0.1-SNAPSHOT" +org-bstats-bstats-api = "3.1.0" + +[libraries] +com-github-earthcow-javadiscordwebhook = { module = "com.github.EarthCow:JavaDiscordWebhook", version.ref = "com-github-earthcow-javadiscordwebhook" } +dev-dejvokep-boosted-yaml = { module = "dev.dejvokep:boosted-yaml", version.ref = "dev-dejvokep-boosted-yaml" } +org-geysermc-floodgate-api = { module = "org.geysermc.floodgate:api", version.ref = "org-geysermc-floodgate-api" } +org-jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "org-jetbrains-annotations" } +org-spigotmc-spigot-api = { module = "org.spigotmc:spigot-api", version.ref = "org-spigotmc-spigot-api" } +org-bstats-bstats-api = { module = "org.bstats:bstats-bukkit", version.ref = "org-bstats-bstats-api" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f8e1ee3 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bad7c24 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# 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. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# 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 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/HEAD/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 +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 + +# 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 + +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 ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# 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 + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + 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" && ! "$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 + +# 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" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + 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 + # 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 +fi + + +# 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 new file mode 100755 index 0000000..e509b2d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +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 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +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 + + + +@rem Execute Gradle +"%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% 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! +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 + +:omega diff --git a/bin/ThemisAPI.jar b/libs/ThemisAPI.jar similarity index 100% rename from bin/ThemisAPI.jar rename to libs/ThemisAPI.jar diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 4b059cb..0000000 --- a/pom.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - 4.0.0 - - xyz.earthcow - ThemisToDiscord - 0.3.0 - jar - - ThemisToDiscord - - - 1.8 - UTF-8 - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - - dev.dejvokep.boostedyaml - xyz.earthcow.libs - - - - - - package - - shade - - - false - - - - - - - - src/main/resources - true - - - - - - - spigotmc-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - - - jitpack.io - https://jitpack.io - - - - opencollab-snapshot - https://repo.opencollab.dev/main/ - - - - - - org.spigotmc - spigot-api - 1.17-R0.1-SNAPSHOT - provided - - - org.jetbrains - annotations - 24.1.0 - provided - - - - com.gmail.olexorus.themis.api - ThemisAPI - 0.15.3 - system - ${project.basedir}/bin/ThemisAPI.jar - - - org.geysermc.floodgate - api - 2.2.4-SNAPSHOT - provided - - - dev.dejvokep - boosted-yaml - 1.3.7 - - - com.github.EarthCow - JavaDiscordWebhook - master-SNAPSHOT - - - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..a371f28 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "ThemisToDiscord" diff --git a/src/main/java/xyz/earthcow/themistodiscord/Configuration.java b/src/main/java/xyz/earthcow/themistodiscord/Configuration.java index 5d2f61a..2502f58 100644 --- a/src/main/java/xyz/earthcow/themistodiscord/Configuration.java +++ b/src/main/java/xyz/earthcow/themistodiscord/Configuration.java @@ -6,6 +6,7 @@ import dev.dejvokep.boostedyaml.settings.general.GeneralSettings; import dev.dejvokep.boostedyaml.settings.loader.LoaderSettings; import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings; +import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; @@ -14,40 +15,44 @@ import java.util.stream.Collectors; public class Configuration { - private YamlDocument config; + @NotNull + private final ThemisToDiscord ttd; + @NotNull + private final YamlDocument config; + @NotNull + private final Utils utils; private Set messages; - public Configuration() { - ThemisToDiscord plugin = ThemisToDiscord.instance; + public Configuration(@NotNull ThemisToDiscord ttd) throws IOException { + this.ttd = ttd; - try { - config = YamlDocument.create( - new File(plugin.getDataFolder(), "config.yml"), - Objects.requireNonNull(plugin.getResource("config.yml")), - GeneralSettings.DEFAULT, - LoaderSettings.builder().setAutoUpdate(true).build(), DumperSettings.DEFAULT, - UpdaterSettings.builder().setVersioning(new BasicVersioning("config-version")) - .setOptionSorting(UpdaterSettings.OptionSorting.SORT_BY_DEFAULTS) - .build() - ); + config = YamlDocument.create( + new File(ttd.getDataFolder(), "config.yml"), + Objects.requireNonNull(ttd.getResource("config.yml")), + GeneralSettings.DEFAULT, + LoaderSettings.builder().setAutoUpdate(true).build(), DumperSettings.DEFAULT, + UpdaterSettings.builder().setVersioning(new BasicVersioning("config-version")) + .setOptionSorting(UpdaterSettings.OptionSorting.SORT_BY_DEFAULTS) + .build() + ); + config.update(); + config.save(); - config.update(); - config.save(); - load(); - } catch (IOException e){ - ThemisToDiscord.log(LogLevel.ERROR, "Could not create/load plugin config, disabling! Additional info: \n" + e); - plugin.getPluginLoader().disablePlugin(plugin); - return; - } + this.utils = new Utils(ttd, config); - if (ThemisToDiscord.isInvalidWebhookUrl(config.getString("webhookUrl"))) { - ThemisToDiscord.log(LogLevel.WARN, "Webhook url is missing or invalid! Set one using /ttd url "); - } + load(); } private void load() { - messages = config.getSection("Messages").getRoutesAsStrings(false).stream().map(route -> new Message(config.getSection("Messages").getSection(route))).collect(Collectors.toSet()); + messages = config.getSection("Messages").getRoutesAsStrings(false).stream() + .map(route -> + new Message(ttd, utils, config.getSection("Messages").getSection(route)) + ) + .collect(Collectors.toSet()); + if (Utils.isInvalidWebhookUrl(config.getString("webhookUrl"))) { + ttd.log(LogLevel.WARN, "Webhook url is missing or invalid! Set one using /ttd url "); + } } public YamlDocument get() { @@ -62,7 +67,7 @@ public void save() { try { config.save(); } catch (IOException e) { - ThemisToDiscord.log(LogLevel.ERROR, "Failed to save plugin config! Additional info: \n" + e); + ttd.log(LogLevel.ERROR, "Failed to save plugin config! Additional info: \n" + e); } } @@ -73,11 +78,8 @@ public void reload() { } config.reload(); load(); - if (ThemisToDiscord.isInvalidWebhookUrl(config.getString("webhookUrl"))) { - ThemisToDiscord.log(LogLevel.WARN, "Webhook url is missing or invalid! Set one using /ttd url "); - } } catch (IOException e) { - ThemisToDiscord.log(LogLevel.ERROR, "Failed to reload plugin config! Additional info: \n" + e); + ttd.log(LogLevel.ERROR, "Failed to reload plugin config! Additional info: \n" + e); } } diff --git a/src/main/java/xyz/earthcow/themistodiscord/EmbedUtil.java b/src/main/java/xyz/earthcow/themistodiscord/EmbedUtil.java new file mode 100644 index 0000000..93e8d75 --- /dev/null +++ b/src/main/java/xyz/earthcow/themistodiscord/EmbedUtil.java @@ -0,0 +1,78 @@ +package xyz.earthcow.themistodiscord; + +import dev.dejvokep.boostedyaml.block.implementation.Section; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.earthcow.discordwebhook.DiscordWebhook; + +import java.awt.*; +import java.util.List; + +public class EmbedUtil { + + public static void applyColor(@NotNull DiscordWebhook.EmbedObject embed, @NotNull String color, @NotNull ThemisToDiscord ttd, @NotNull String msgName) { + if (color.contains("%category_color%")) { + embed.setColorStr(color); + } else { + try { + embed.setColor( + Color.decode(color) + ); + } catch (NumberFormatException e) { + ttd.log(LogLevel.WARN, "Invalid color string: " + color + " for message: " + msgName + ". Using black."); + ttd.log(LogLevel.DEBUG, "Exception: " + e); + embed.setColor(Color.BLACK); + } + } + } + + public static void applyAuthor(@NotNull DiscordWebhook.EmbedObject embed, @Nullable Section section) { + if (section != null) { + embed.setAuthor( + section.getString("Name"), + section.getString("Url"), + section.getString("ImageUrl") + ); + } + } + + public static void applyTitle(@NotNull DiscordWebhook.EmbedObject embed, @Nullable Section section) { + if (section != null) { + embed.setTitle(section.getString("Text")); + embed.setUrl(section.getString("Url")); + } + } + + public static void applyFields(@NotNull DiscordWebhook.EmbedObject embed, @NotNull List fields) { + if (!fields.isEmpty()) { + + for (String field : fields) { + if (field.contains(";")) { + + String[] parts = field.split(";"); + if (parts.length < 2) { + continue; + } + + boolean inline = parts.length < 3 || Boolean.parseBoolean(parts[2]); + + embed.addField(parts[0], parts[1], inline); + } else { + boolean inline = Boolean.parseBoolean(field); + embed.addField("\u200e", "\u200e", inline); + } + } + + } + } + + public static void applyFooter(@NotNull DiscordWebhook.EmbedObject embed, @Nullable Section section) { + if (section != null) { + embed.setFooter( + section.getString("Text"), + section.getString("IconUrl") + ); + } + } + +} diff --git a/src/main/java/xyz/earthcow/themistodiscord/HandlingService.java b/src/main/java/xyz/earthcow/themistodiscord/HandlingService.java new file mode 100644 index 0000000..5d806b5 --- /dev/null +++ b/src/main/java/xyz/earthcow/themistodiscord/HandlingService.java @@ -0,0 +1,96 @@ +package xyz.earthcow.themistodiscord; + +import com.gmail.olexorus.themis.api.CheckType; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import dev.dejvokep.boostedyaml.block.implementation.Section; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class HandlingService { + private record CheckKey(UUID uuid, CheckType checkType) {} + private record CheckData(long lastSent, double repetitionCount) {} + + private final Cache playerCheckData; + + private final double executionThreshold; + private final double repetitionDelay; + private final double repetitionThreshold; + + private static final long DEFAULT_LAST_SENT = 0L; + private static final double DEFAULT_REPETITION_COUNT = 0.0; + + public HandlingService(@NotNull Section handlingSection, @NotNull String msgName, @NotNull ThemisToDiscord ttd) { + double localExecutionThreshold = handlingSection.getDouble("execution-threshold"); + double localRepetitionDelay = handlingSection.getDouble("repetition-delay"); + double localRepetitionThreshold = handlingSection.getDouble("repetition-threshold"); + if (localExecutionThreshold < 1) { + ttd.log(LogLevel.WARN, "Execution threshold must be positive! Message: " + msgName + " will use the default of 10.0"); + localExecutionThreshold = 10.0; + } + if (localRepetitionDelay < 1 || localRepetitionDelay >= 86400) { + ttd.log(LogLevel.WARN, "Repetition delay must be positive and less than 24 hours! Message: " + msgName + " will use the default of 10.0"); + localRepetitionDelay = 10.0; + } + if (localRepetitionThreshold < 1) { + ttd.log(LogLevel.WARN, "Repetition threshold must be positive! Message: " + msgName + " will use the default of 5.0"); + localRepetitionThreshold = 5.0; + } + this.executionThreshold = localExecutionThreshold; + this.repetitionDelay = localRepetitionDelay; + this.repetitionThreshold = localRepetitionThreshold; + + this.playerCheckData = CacheBuilder.newBuilder() + .expireAfterAccess(24, TimeUnit.HOURS) + .build(); + } + + public double getExecutionThreshold() { + return executionThreshold; + } + + public double getRepetitionDelay() { + return repetitionDelay; + } + + public double getRepetitionThreshold() { + return repetitionThreshold; + } + + public long getLastSentTimeForPlayer(Player player, CheckType checkType) { + CheckData checkData = playerCheckData.getIfPresent(new CheckKey(player.getUniqueId(), checkType)); + if (checkData == null) return DEFAULT_LAST_SENT; + return checkData.lastSent; + } + + public void updateLastSentTimeForPlayer(Player player, CheckType checkType) { + CheckKey playerCheckDataKey = new CheckKey(player.getUniqueId(), checkType); + CheckData checkData = playerCheckData.getIfPresent(playerCheckDataKey); + if (checkData == null) { + checkData = new CheckData(System.currentTimeMillis(), DEFAULT_REPETITION_COUNT); + } else { + checkData = new CheckData(System.currentTimeMillis(), checkData.repetitionCount); + } + playerCheckData.put(playerCheckDataKey, checkData); + } + + public double getRepetitionCountForPlayer(Player player, CheckType checkType) { + CheckData checkData = playerCheckData.getIfPresent(new CheckKey(player.getUniqueId(), checkType)); + if (checkData == null) return DEFAULT_REPETITION_COUNT; + return checkData.repetitionCount; + } + + public void putRepetitionCountForPlayer(Player player, CheckType checkType, double repetitionCount) { + CheckKey playerCheckDataKey = new CheckKey(player.getUniqueId(), checkType); + CheckData checkData = playerCheckData.getIfPresent(playerCheckDataKey); + if (checkData == null) { + checkData = new CheckData(DEFAULT_LAST_SENT, repetitionCount); + } else { + checkData = new CheckData(checkData.lastSent, repetitionCount); + } + playerCheckData.put(playerCheckDataKey, checkData); + } +} diff --git a/src/main/java/xyz/earthcow/themistodiscord/Message.java b/src/main/java/xyz/earthcow/themistodiscord/Message.java index e5113bb..55f101f 100644 --- a/src/main/java/xyz/earthcow/themistodiscord/Message.java +++ b/src/main/java/xyz/earthcow/themistodiscord/Message.java @@ -1,279 +1,159 @@ package xyz.earthcow.themistodiscord; -import com.gmail.olexorus.themis.api.CheckType; import dev.dejvokep.boostedyaml.block.implementation.Section; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.slf4j.helpers.Util; import xyz.earthcow.discordwebhook.DiscordWebhook; -import java.awt.*; import java.io.FileNotFoundException; import java.io.IOException; -import java.util.*; -import java.util.List; +import java.time.temporal.TemporalAccessor; +import java.util.Date; +import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Message { + @NotNull + private final ThemisToDiscord ttd; + @NotNull + private final Utils utils; + @NotNull private final Section message; @NotNull private final String name; @NotNull private final String webhookUrl; - @NotNull - private DiscordWebhook webhook; + private final String webhookJson; @Nullable - private final Section handling; - - // For use with handling - private final HashMap> lastSentTimesPerPlayer = new HashMap<>(); - private final HashMap> repetitionCountersPerPlayer = new HashMap<>(); + private final HandlingService handling; + @NotNull private final ExecutorService executor = Executors.newSingleThreadExecutor(); - public Message(@NotNull Section message) { + @Nullable + private String originalTimestamp; + + public Message(@NotNull ThemisToDiscord ttd, @NotNull Utils utils, @NotNull Section message) { + this.ttd = ttd; + this.utils = utils; + this.message = message; - this.name = message.getNameAsString(); + this.name = Objects.requireNonNull(message.getNameAsString()); + + this.webhookUrl = determineWebhookUrl(); + + // Determine and set the webhook json + String jsonString = message.getString("Json", ""); + if (!jsonString.isEmpty() && !jsonString.equals("{}")) { + this.webhookJson = jsonString; + } else { + this.webhookJson = getJsonWebhook(); + } + + // Define the handling service + Section handlingSection = message.getSection("Handling", null); + if (handlingSection != null && handlingSection.getBoolean("Enabled", false)) { + this.handling = new HandlingService(handlingSection, name, ttd); + } else { + this.handling = null; + } + } - // Discover the webhook url to be used + private String determineWebhookUrl() { String localWebhookUrl; if (message.getBoolean("CustomWebhook.Enabled", false)) { localWebhookUrl = message.getString("CustomWebhook.Url", ""); - if (ThemisToDiscord.isInvalidWebhookUrl(localWebhookUrl)) { + if (Utils.isInvalidWebhookUrl(localWebhookUrl)) { if (!localWebhookUrl.isEmpty()) { - ThemisToDiscord.log(LogLevel.WARN, "Invalid custom webhook url for message: " + name + "! This message will use the global webhook url."); + ttd.log(LogLevel.WARN, "Invalid custom webhook url for message: " + name + "! This message will use the global webhook url."); } localWebhookUrl = message.getRoot().getString("webhookUrl"); } } else { localWebhookUrl = message.getRoot().getString("webhookUrl"); } - // Set the webhook url for this message - this.webhookUrl = localWebhookUrl; - this.webhook = new DiscordWebhook(localWebhookUrl); - - // Define the handling section - this.handling = message.getSection("Handling", null); - + return localWebhookUrl; } - private void handleMessageContent( - @NotNull Player player, - @NotNull String detectionType, - double score, - double ping, - double tps - ) { - // Define a new webhook object to clear previous contents (mostly for embeds) - this.webhook = new DiscordWebhook(webhookUrl); + private String getJsonWebhook() { + DiscordWebhook webhook = new DiscordWebhook(); // Set the custom webhook parameters if (message.getBoolean("CustomWebhook.Enabled", false)) { - webhook.setUsername( - Utils.handleAllPlaceholders( - message.getString("CustomWebhook.Name"), - player, detectionType, score, ping, tps) - ); - webhook.setAvatarUrl( - Utils.handleAllPlaceholders( - message.getString("CustomWebhook.AvatarUrl"), - player, detectionType, score, ping, tps) - ); + webhook.setUsername(message.getString("CustomWebhook.Name")); + webhook.setAvatarUrl(message.getString("CustomWebhook.AvatarUrl")); } // Set the message content - webhook.setContent( - Utils.handleAllPlaceholders( - message.getString("Content"), - player, detectionType, score, ping, tps - ) - ); + webhook.setContent(message.getString("Content")); Section embedSection = message.getSection("Embed"); if (embedSection == null) { - return; + return webhook.getJsonString(); } + webhook.addEmbed(getEmbedFromConfig(embedSection)); + return webhook.getJsonString(); + } + + private DiscordWebhook.EmbedObject getEmbedFromConfig(@NotNull Section embedSection) { // Define the embed object DiscordWebhook.EmbedObject embed = new DiscordWebhook.EmbedObject(); // Set the color - String colorStr = embedSection.getString("Color").replaceAll("%category_color%", message.getRoot().getString("categoryColors." + detectionType)); - try { - embed.setColor( - Color.decode(colorStr) - ); - } catch (NumberFormatException e) { - ThemisToDiscord.log(LogLevel.WARN, "Invalid color string: " + colorStr + " for message: " + name + ". Using black."); - ThemisToDiscord.log(LogLevel.DEBUG, "Exception: " + e); - embed.setColor(Color.BLACK); - } - + EmbedUtil.applyColor(embed, embedSection.getString("Color", ""), ttd, name); // Set the author - if (embedSection.get("Author", null) != null) { - embed.setAuthor( - Utils.handleAllPlaceholders( - embedSection.getString("Author.Name"), - player, detectionType, score, ping, tps - ), - Utils.handleAllPlaceholders( - embedSection.getString("Author.Url"), - player, detectionType, score, ping, tps - ), - Utils.handleAllPlaceholders( - embedSection.getString("Author.ImageUrl"), - player, detectionType, score, ping, tps - ) - ); - } - + EmbedUtil.applyAuthor(embed, embedSection.getSection("Author")); // Set the thumbnail url - embed.setThumbnail(embedSection.getString("ThumbnailUrl", null)); - + embed.setThumbnail(embedSection.getString("ThumbnailUrl")); // Set the title - if (embedSection.get("Title", null) != null) { - embed.setTitle( - Utils.handleAllPlaceholders( - embedSection.getString("Title.Text", null), - player, detectionType, score, ping, tps - ) - ); - embed.setUrl( - Utils.handleAllPlaceholders( - embedSection.getString("Title.Url", null), - player, detectionType, score, ping, tps - ) - ); - } - + EmbedUtil.applyTitle(embed, embedSection.getSection("Title")); // Set the description - embed.setDescription( - Utils.handleAllPlaceholders( - embedSection.getString("Description", null), - player, detectionType, score, ping, tps - ) - ); - + embed.setDescription(embedSection.getString("Description")); // Set the fields - List fields = embedSection.getStringList("Fields"); - if (!fields.isEmpty()) { - - for (String field : fields) { - if (field.contains(";")) { - - String[] parts = field.split(";"); - if (parts.length < 2) { - continue; - } - - boolean inline = parts.length < 3 || Boolean.parseBoolean(parts[2]); - - embed.addField( - Utils.handleAllPlaceholders( - parts[0], - player, detectionType, score, ping, tps - ), - Utils.handleAllPlaceholders( - parts[1], - player, detectionType, score, ping, tps - ), - inline - ); - } else { - boolean inline = Boolean.parseBoolean(field); - embed.addField("\u200e", "\u200e", inline); - } - } - - } - + EmbedUtil.applyFields(embed, embedSection.getStringList("Fields")); // Set the image url - embed.setImage( - Utils.handleAllPlaceholders( - embedSection.getString("ImageUrl", null), - player, detectionType, score, ping, tps - ) - ); - + embed.setImage(embedSection.getString("ImageUrl")); // Set the footer - if (embedSection.get("Footer", null) != null) { - embed.setFooter( - Utils.handleAllPlaceholders( - embedSection.getString("Footer.Text", null), - player, detectionType, score, ping, tps - ), - Utils.handleAllPlaceholders( - embedSection.getString("Footer.IconUrl", null), - player, detectionType, score, ping, tps - ) - ); - } + EmbedUtil.applyFooter(embed, embedSection.getSection("Footer")); // Set the timestamp if (embedSection.getBoolean("Timestamp")) { - embed.setTimestamp((new Date()).toInstant()); + TemporalAccessor ta = (new Date()).toInstant(); + embed.setTimestamp(ta); + this.originalTimestamp = ta.toString(); } - webhook.addEmbed(embed); + return embed; } public void execute(@NotNull Player player, @NotNull String detectionType, double score, double ping, double tps, @Nullable CommandSender sender) { // Using a single thread executor ensures messages are not concurrently modified and sent in succession executor.submit(() -> { try { - String jsonString = message.getString("Json", ""); - if (jsonString.isEmpty() || jsonString.equals("{}")) { - handleMessageContent(player, detectionType, score, ping, tps); - webhook.execute(); - } else { - webhook.execute(Utils.handleAllPlaceholders(jsonString, player, detectionType, score, ping, tps)); + String jsonPayload = Objects.requireNonNull( + utils.handleAllPlaceholders(webhookJson, player, detectionType, score, ping, tps) + ); + if (originalTimestamp != null) { + jsonPayload = jsonPayload.replace(originalTimestamp, (new Date()).toInstant().toString()); } + DiscordWebhook.execute(webhookUrl, jsonPayload); if (sender != null) { sender.sendMessage(ChatColor.GREEN + "Message: " + name + ", was sent!"); } } catch (IOException e) { - String msg = null; + String msg; if (e instanceof FileNotFoundException) { msg = "Your webhook url is not valid! Update it with /ttd url !"; } else { - String message = e.getMessage(); - if (message != null && message.contains("HTTP response code:")) { - try { - int responseCode = Integer.parseInt(message.substring(message.indexOf(":") + 2, message.indexOf(":") + 5)); - switch (responseCode) { - case 400: - msg = "Error - 400 response - bad request. Verify all urls are either blank or valid urls."; - break; - case 401: - msg = "Error - 401 response - unauthorized. Verify webhook url and discord server status."; - break; - case 403: - msg = "Error - 403 response - forbidden. Verify webhook url and discord server status."; - break; - case 404: - msg = "Error - 404 response - not found. Verify webhook url and discord server status."; - break; - case 429: - msg = "Error - 429 response - too many requests. This webhook has sent too many messages in too short amount of time."; - break; - case 500: - msg = "Error - 505 response - internal server error. Discord services may be temporarily down."; - break; - default: - msg = "Error - " + responseCode + " response - unexpected error code."; - break; - } - } catch (IndexOutOfBoundsException | NumberFormatException ex) { - ThemisToDiscord.log(LogLevel.DEBUG, "Secondary exception: " + ex); - } - } + msg = getHttpErrorMsg(e.getMessage()); } if (msg == null) { msg = "Unknown error has occurred. Please make a bug report at https://github.com/RagingTech/ThemisToDiscord/issues."; @@ -282,18 +162,46 @@ public void execute(@NotNull Player player, @NotNull String detectionType, doubl if (sender != null) { sender.sendMessage(ChatColor.RED + msg); } - ThemisToDiscord.log(LogLevel.ERROR, msg); - ThemisToDiscord.log(LogLevel.DEBUG, "Exception: " + e); - ThemisToDiscord.log(LogLevel.DEBUG, "Webhook: " + webhook.getJsonString()); + ttd.log(LogLevel.ERROR, msg); + ttd.log(LogLevel.DEBUG, "Exception: " + e); + ttd.log(LogLevel.DEBUG, "Webhook: " + webhookJson); } }); } + @Nullable + private String getHttpErrorMsg(@Nullable String message) { + String msg = null; + if (message != null && message.contains("HTTP response code:")) { + try { + int responseCode = Integer.parseInt(message.substring(message.indexOf(":") + 2, message.indexOf(":") + 5)); + msg = switch (responseCode) { + case 400 -> + "Error - 400 response - bad request. Verify all urls are either blank or valid urls."; + case 401 -> + "Error - 401 response - unauthorized. Verify webhook url and discord server status."; + case 403 -> + "Error - 403 response - forbidden. Verify webhook url and discord server status."; + case 404 -> + "Error - 404 response - not found. Verify webhook url and discord server status."; + case 429 -> + "Error - 429 response - too many requests. This webhook has sent too many messages in too short amount of time."; + case 500 -> + "Error - 505 response - internal server error. Discord services may be temporarily down."; + default -> "Error - " + responseCode + " response - unexpected error code."; + }; + } catch (IndexOutOfBoundsException | NumberFormatException ex) { + ttd.log(LogLevel.DEBUG, "Secondary exception: " + ex); + } + } + return msg; + } + public @NotNull String getName() { return name; } - public @Nullable Section getHandling() { + public @Nullable HandlingService getHandlingService() { return handling; } @@ -301,36 +209,8 @@ public void forceExecutorShutdown() { // Should only be performed upon reload int unsentMessages = executor.shutdownNow().size(); if (unsentMessages > 0) { - ThemisToDiscord.log(LogLevel.WARN, unsentMessages + " messages were cancelled. For message: " + name); - } - } - - public long getLastSentTimeForPlayer(Player player, CheckType checkType) { - HashMap times = lastSentTimesPerPlayer.get(player.getUniqueId()); - if (times == null) { - return 0; + ttd.log(LogLevel.WARN, unsentMessages + " messages were cancelled. For message: " + name); } - return times.getOrDefault(checkType, 0L); - } - - public void updateLastSentTimeForPlayer(Player player, CheckType checkType) { - HashMap times = lastSentTimesPerPlayer.getOrDefault(player.getUniqueId(), new HashMap<>()); - times.put(checkType, System.currentTimeMillis()); - lastSentTimesPerPlayer.put(player.getUniqueId(), times); - } - - public int getRepetitionCountForPlayer(Player player, CheckType checkType) { - HashMap repetitionCounts = repetitionCountersPerPlayer.get(player.getUniqueId()); - if (repetitionCounts == null) { - return -2; - } - return repetitionCounts.getOrDefault(checkType, -2); - } - - public void putRepetitionCountForPlayer(Player player, CheckType checkType, int repetitionCount) { - HashMap repetitionCounts = repetitionCountersPerPlayer.getOrDefault(player.getUniqueId(), new HashMap<>()); - repetitionCounts.put(checkType, repetitionCount); - repetitionCountersPerPlayer.put(player.getUniqueId(), repetitionCounts); } } diff --git a/src/main/java/xyz/earthcow/themistodiscord/ThemisListener.java b/src/main/java/xyz/earthcow/themistodiscord/ThemisListener.java index 3962a6b..75c3dfa 100644 --- a/src/main/java/xyz/earthcow/themistodiscord/ThemisListener.java +++ b/src/main/java/xyz/earthcow/themistodiscord/ThemisListener.java @@ -3,16 +3,25 @@ import com.gmail.olexorus.themis.api.CheckType; import com.gmail.olexorus.themis.api.ThemisApi; import com.gmail.olexorus.themis.api.ViolationEvent; -import dev.dejvokep.boostedyaml.block.implementation.Section; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; public class ThemisListener implements Listener { + @NotNull + private final ThemisToDiscord ttd; + @NotNull + private final Configuration config; private boolean pingSupportedVersion = true; + public ThemisListener(@NotNull ThemisToDiscord ttd, @NotNull Configuration config) { + this.ttd = ttd; + this.config = config; + } + @EventHandler - public void onViolationEvent(ViolationEvent event) { + public void onViolationEvent(@NotNull ViolationEvent event) { Player player = event.getPlayer(); CheckType checkType = event.getType(); @@ -22,37 +31,36 @@ public void onViolationEvent(ViolationEvent event) { ping = (ThemisApi.getPing(player) != null) ? ThemisApi.getPing(player) : 0; tps = ThemisApi.getTps(); } catch (NoSuchMethodError e) { - ThemisToDiscord.log(LogLevel.WARN, "Please update Themis to 0.15.3 or higher for player ping and server tps!"); + ttd.log(LogLevel.WARN, "Please update Themis to 0.15.3 or higher for player ping and server tps!"); pingSupportedVersion = false; } } double score = Math.round(ThemisApi.getViolationScore(player, checkType) * 100.0) / 100.0; - for (Message message : ThemisToDiscord.config.getMessages()) { - Section handling = message.getHandling(); + for (Message message : config.getMessages()) { + HandlingService handling = message.getHandlingService(); - if (handling == null || !handling.getBoolean("Enabled", false)) { + if (handling == null) { continue; } - if (handling.getDouble("execution-threshold") > score - || handling.getDouble("repetition-delay") > ((System.currentTimeMillis() - message.getLastSentTimeForPlayer(player, checkType)) / 1000.0)) + if (handling.getExecutionThreshold() > score + || handling.getRepetitionDelay() > ((System.currentTimeMillis() - handling.getLastSentTimeForPlayer(player, checkType)) / 1000.0)) continue; - int repetitionCounterForCheckType = message.getRepetitionCountForPlayer(player, checkType) + 1; + double currentRepCount = handling.getRepetitionCountForPlayer(player, checkType) + 1.0; + + if (currentRepCount >= handling.getRepetitionThreshold()) { + message.execute(player, checkType.getDescription(), score, ping, tps, null); + handling.updateLastSentTimeForPlayer(player, checkType); - if (repetitionCounterForCheckType == handling.getDouble("repetition-threshold")) { - message.putRepetitionCountForPlayer(player, checkType, -1); - repetitionCounterForCheckType = -1; + // Reset the counter + handling.putRepetitionCountForPlayer(player, checkType, 0); } else { - message.putRepetitionCountForPlayer(player, checkType, repetitionCounterForCheckType); + // Update the counter + handling.putRepetitionCountForPlayer(player, checkType, currentRepCount); } - - if (repetitionCounterForCheckType != -1) continue; - - message.execute(player, checkType.getDescription(), score, ping, tps, null); - message.updateLastSentTimeForPlayer(player, checkType); } } } \ No newline at end of file diff --git a/src/main/java/xyz/earthcow/themistodiscord/ThemisToDiscord.java b/src/main/java/xyz/earthcow/themistodiscord/ThemisToDiscord.java index c34cab9..01a102b 100644 --- a/src/main/java/xyz/earthcow/themistodiscord/ThemisToDiscord.java +++ b/src/main/java/xyz/earthcow/themistodiscord/ThemisToDiscord.java @@ -1,56 +1,61 @@ package xyz.earthcow.themistodiscord; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.SimplePie; import org.bukkit.plugin.java.JavaPlugin; -import org.geysermc.floodgate.api.FloodgateApi; -import org.jetbrains.annotations.Nullable; -import xyz.earthcow.discordwebhook.DiscordWebhook; +import java.io.IOException; import java.util.Objects; public final class ThemisToDiscord extends JavaPlugin { - public static ThemisToDiscord instance; - public static Configuration config; - public static FloodgateApi floodgateApi; + private Configuration config; @Override public void onEnable() { - instance = this; - - config = new Configuration(); + try { + this.config = new Configuration(this); + } catch (IOException e){ + log(LogLevel.ERROR, "Could not create/load plugin config, disabling! Additional info: \n" + e); + getPluginLoader().disablePlugin(this); + return; + } - TtdCommand ttdCommand = new TtdCommand(); + TtdCommand ttdCommand = new TtdCommand(config); Objects.requireNonNull(getCommand("ttd")).setExecutor(ttdCommand); Objects.requireNonNull(getCommand("ttd")).setTabCompleter(ttdCommand); - getServer().getPluginManager().registerEvents(new ThemisListener(), this); + getServer().getPluginManager().registerEvents(new ThemisListener(this, config), this); - if (getServer().getPluginManager().isPluginEnabled("Floodgate")) { - log("Found Floodgate! Enabling features..."); - floodgateApi = FloodgateApi.getInstance(); - } else { - log(LogLevel.WARN, "Floodgate not found! Some features may be disabled."); - } + int pluginId = 28743; + Metrics metrics = new Metrics(this, pluginId); + metrics.addCustomChart(new SimplePie("message_count", () -> String.valueOf(config.getMessages().size()))); } @Override - public void onDisable() {} + public void onDisable() { + if (config != null) { + for (Message message : config.getMessages()) { + message.forceExecutorShutdown(); + } + } + } - public static void log(String message) { - instance.getLogger().info(message); + public void log(String message) { + getLogger().info(message); } - public static void log(LogLevel logLevel, String message) { + public void log(LogLevel logLevel, String message) { switch (logLevel) { case DEBUG: if (config.get().getBoolean("debug")) { - instance.getLogger().warning("[DEBUG] " + message); + getLogger().warning("[DEBUG] " + message); } break; case WARN: - instance.getLogger().warning(message); + getLogger().warning(message); break; case ERROR: - instance.getLogger().severe(message); + getLogger().severe(message); break; default: log(message); @@ -58,8 +63,4 @@ public static void log(LogLevel logLevel, String message) { } } - public static boolean isInvalidWebhookUrl(@Nullable String url) { - if (url == null) return true; - return !DiscordWebhook.WEBHOOK_PATTERN.matcher(url).matches(); - } } diff --git a/src/main/java/xyz/earthcow/themistodiscord/TtdCommand.java b/src/main/java/xyz/earthcow/themistodiscord/TtdCommand.java index a4c4286..d3afa69 100644 --- a/src/main/java/xyz/earthcow/themistodiscord/TtdCommand.java +++ b/src/main/java/xyz/earthcow/themistodiscord/TtdCommand.java @@ -15,6 +15,12 @@ import java.util.stream.Collectors; public class TtdCommand implements CommandExecutor, TabCompleter { + @NotNull + private final Configuration config; + + public TtdCommand(@NotNull Configuration config) { + this.config = config; + } @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String cmd, @NotNull String[] args) { @@ -27,14 +33,14 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } String webhookUrl = args[1]; - if (ThemisToDiscord.isInvalidWebhookUrl(webhookUrl)) { + if (Utils.isInvalidWebhookUrl(webhookUrl)) { sender.sendMessage(ChatColor.RED + "That is not a valid webhook url!"); return true; } - ThemisToDiscord.config.get().set("webhookUrl", webhookUrl); - ThemisToDiscord.config.save(); + config.get().set("webhookUrl", webhookUrl); + config.save(); // Re-evaluate config messages for changes in webhook url - ThemisToDiscord.config.reload(); + config.reload(); sender.sendMessage(ChatColor.GREEN + "Successfully set the webhook url!"); break; case "msg": @@ -43,7 +49,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command return true; } - Message message = ThemisToDiscord.config.getMessages().stream().filter(msg -> msg.getName().equals(args[1])).findFirst().orElse(null); + Message message = config.getMessages().stream().filter(msg -> msg.getName().equals(args[1])).findFirst().orElse(null); if (message == null) { sender.sendMessage(ChatColor.RED + "The message: " + args[1] + " does not exist!"); return true; @@ -78,7 +84,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command message.execute(player, type, score, ping, tps, (sender instanceof Player) ? sender : null); break; case "reload": - ThemisToDiscord.config.reload(); + config.reload(); sender.sendMessage(ChatColor.GREEN + "Successfully reloaded the configuration file!"); break; default: @@ -135,7 +141,7 @@ public List onTabComplete(@NotNull CommandSender sender, @NotNull Comman if (args[0].equalsIgnoreCase("msg")) { if (args.length == 2) { - return ThemisToDiscord.config.getMessages().stream().map(Message::getName).collect(Collectors.toList()); + return config.getMessages().stream().map(Message::getName).collect(Collectors.toList()); } List completions = new ArrayList<>(); diff --git a/src/main/java/xyz/earthcow/themistodiscord/Utils.java b/src/main/java/xyz/earthcow/themistodiscord/Utils.java index 880afae..a69c868 100644 --- a/src/main/java/xyz/earthcow/themistodiscord/Utils.java +++ b/src/main/java/xyz/earthcow/themistodiscord/Utils.java @@ -1,64 +1,48 @@ package xyz.earthcow.themistodiscord; +import dev.dejvokep.boostedyaml.YamlDocument; import org.bukkit.entity.Player; +import org.geysermc.floodgate.api.FloodgateApi; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import xyz.earthcow.discordwebhook.DiscordWebhook; public class Utils { - + @NotNull + private ThemisToDiscord ttd; @Nullable - public static String handleFloodgatePlaceholders(@Nullable String str, @NotNull Player player) { - if (str == null) {return null;} + private FloodgateApi floodgateApi; + @NotNull + private final YamlDocument config; + + public Utils(@NotNull ThemisToDiscord ttd, @NotNull YamlDocument configDocument) { + this.ttd = ttd; + if (ttd.getServer().getPluginManager().isPluginEnabled("Floodgate")) { + ttd.log("Found Floodgate! Enabling features..."); + floodgateApi = FloodgateApi.getInstance(); + } else { + ttd.log(LogLevel.WARN, "Floodgate not found! Some features may be disabled."); + } + this.config = configDocument; + } + + @NotNull + private String getPlayerOs(@NotNull Player player) { String os; - if (ThemisToDiscord.floodgateApi == null) { + if (floodgateApi == null) { os = "install_floodgate"; } else { - if (ThemisToDiscord.floodgateApi.isFloodgatePlayer(player.getUniqueId())) { - os = ThemisToDiscord.floodgateApi.getPlayer(player.getUniqueId()).getDeviceOs() + ""; + if (floodgateApi.isFloodgatePlayer(player.getUniqueId())) { + os = floodgateApi.getPlayer(player.getUniqueId()).getDeviceOs().toString(); } else { os = "Java"; } } - return str - .replaceAll("%os%", os); - } - - @Nullable - public static String handleAvatarUrlPlaceholders(@Nullable String str, @NotNull Player player) { - if (str == null) {return null;} - return str - .replaceAll( - "%avatar_url%", - handlePlayerPlaceholders(ThemisToDiscord.config.get().getString("AvatarUrl"), player) - ); - } - - @Nullable - public static String handlePlayerPlaceholders(@Nullable String str, @NotNull Player player) { - if (str == null) {return null;} - return str - .replaceAll("%player_name%", player.getName()) - .replaceAll("%player_uuid%", player.getUniqueId() + ""); + return os; } @Nullable - public static String handleDetectionTypePlaceholders(@Nullable String str, @NotNull String detectionType) { - if (str == null) {return null;} - return str - .replaceAll("%detection_type%", detectionType); - } - - @Nullable - public static String handleStatPlaceholders(@Nullable String str, double score, double ping, double tps) { - if (str == null) {return null;} - return str - .replaceAll("%score%", score + "") - .replaceAll("%ping%", ping + "") - .replaceAll("%tps%", tps + ""); - } - - @Nullable - public static String handleAllPlaceholders( + public String handleAllPlaceholders( @Nullable String str, @NotNull Player player, @NotNull String detectionType, @@ -66,22 +50,33 @@ public static String handleAllPlaceholders( double ping, double tps ) { - if (str == null) {return null;} - return handleStatPlaceholders( - handleDetectionTypePlaceholders( - handlePlayerPlaceholders( - handleAvatarUrlPlaceholders( - handleFloodgatePlaceholders( - str, player - ), - player - ), - player - ), - detectionType - ), - score, ping, tps - ); + if (str == null || str.isEmpty()) { + return str; + } + return str + .replace("%avatar_url%", config.getString("AvatarUrl", "")) + .replace("%os%", getPlayerOs(player)) + .replace("%player_name%", player.getName()) + .replace("%player_uuid%", player.getUniqueId().toString()) + .replace("%detection_type%", detectionType) + .replace("%category_color%", hexToInteger(config.getString("categoryColors." + detectionType, "")).toString()) + .replace("%score%", Double.toString(score)) + .replace("%ping%", Double.toString(ping)) + .replace("%tps%", Double.toString(tps)); } + private Integer hexToInteger(String hex) { + try { + return Integer.parseInt(hex.replace("#", ""), 16); + } catch (NumberFormatException e) { + ttd.log(LogLevel.WARN, "Invalid color string: " + hex + ". Using black."); + ttd.log(LogLevel.DEBUG, "Exception: " + e); + return 0; + } + } + + public static boolean isInvalidWebhookUrl(@Nullable String url) { + if (url == null) return true; + return !DiscordWebhook.WEBHOOK_PATTERN.matcher(url).matches(); + } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 2500bbc..7ea0d46 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ -name: ThemisToDiscord -version: '${project.version}' +name: '${name}' +version: '${version}' main: xyz.earthcow.themistodiscord.ThemisToDiscord api-version: '1.17' depend: