commit ffb5800924ae45fa66f02026cf2cbabc7e777748 Author: HypherionMC Date: Sun May 21 19:58:15 2023 +0200 Initial Library Work done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de13221 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +.idea \ No newline at end of file diff --git a/HEADER b/HEADER new file mode 100644 index 0000000..cb7b631 --- /dev/null +++ b/HEADER @@ -0,0 +1,4 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..13b05c0 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,36 @@ +pipeline { + agent { + label "master" + } + tools { + jdk "JAVA17" + } + stages { + stage("Notify Discord") { + steps { + discordSend webhookURL: env.FDD_WH_ADMIN, + title: "Build Started: SDLink-Core #${BUILD_NUMBER}", + link: env.BUILD_URL, + description: "Build: [${BUILD_NUMBER}](${env.BUILD_URL})" + } + } + stage("Publish") { + steps { + sh "chmod +x ./gradlew" + sh "./gradlew clean spotlessCheck publish" + } + } + } + post { + always { + sh "./gradlew --stop" + deleteDir() + + discordSend webhookURL: env.FDD_WH_ADMIN, + title: "Build Finished: SDLink-Core #${BUILD_NUMBER}", + link: env.BUILD_URL, + result: currentBuild.currentResult, + description: "Build: [${BUILD_NUMBER}](${env.BUILD_URL})\nStatus: ${currentBuild.currentResult}" + } + } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..f0971e5 --- /dev/null +++ b/build.gradle @@ -0,0 +1,152 @@ +plugins { + id 'java' + id 'com.github.johnrengelman.shadow' version '7.0.0' + id 'maven-publish' + id "com.diffplug.spotless" version "6.13.0" +} +apply plugin: 'java' +java.toolchain.languageVersion = JavaLanguageVersion.of(16) + +group 'com.hypherionmc.sdlink' +version = "${version_major}.${version_minor}.${version_patch}" + +configurations { + shaded + shaded.transitive = true + implementation.extendsFrom(shaded) +} + +repositories { + mavenCentral() + maven { url "https://maven.firstdarkdev.xyz/releases" } + maven { url "https://maven.firstdarkdev.xyz/snapshots" } + maven { url "https://m2.dv8tion.net/releases" } + maven { url "https://nexus.velocitypowered.com/repository/maven-public/" } +} + +dependencies { + // Core dependencies, Shaded + + // Discord + shaded("pw.chew:jda-chewtils:${chewtils}") { + exclude group: 'org.apache.commons' + } + shaded("net.dv8tion:JDA:${jda}") { + exclude module: 'opus-java' + exclude group: 'org.apache.commons' + } + shaded("club.minnced:discord-webhooks:${webhooks}") + + // Utilities + shaded("org.apache.commons:commons-collections4:${commons4}") + shaded("com.github.oshi:oshi-core:${oshi}") + shaded("org.jasypt:jasypt:${jasypt}:lite") + shaded("io.jsondb:jsondb-core:${json_db}") + + // Config + shaded("me.hypherionmc.moon-config:core:${moonconfig}") + shaded("me.hypherionmc.moon-config:toml:${moonconfig}") + + + // Compile Only, Not Shaded + + // Logging + implementation("org.apache.logging.log4j:log4j-api:${log4j}") + implementation("org.apache.logging.log4j:log4j-core:${log4j}") + implementation("org.apache.logging.log4j:log4j-slf4j18-impl:${log4j}") + + // Utilities + implementation("org.apache.commons:commons-lang3:${commons}") + implementation("commons-io:commons-io:${commonsio}") + implementation("com.google.code.gson:gson:${gson}") + implementation("com.google.guava:guava:31.1-jre") +} + +shadowJar { + configurations = [project.configurations.shaded] + dependencies { + exclude(dependency('org.apache.logging.log4j:log4j-core:.*')) + exclude(dependency('org.apache.logging.log4j:log4j-core:.*')) + exclude(dependency('org.apache.logging.log4j:log4j-slf4j18-impl:.*')) + exclude(dependency('org.apache.commons:commons-lang3:.*')) + exclude(dependency('com.google.code.gson:.*')) + exclude(dependency('javax:.*')) + exclude(dependency('org.jetbrains:.*')) + exclude(dependency('net.java.dev.jna:.*')) + exclude(dependency('org.slf4j:.*')) + + exclude 'org/slf4j/**' + exclude 'META-INF/versions/9/**' + exclude 'module-info.class' + exclude 'org/apache/commons/lang3/**' + + relocate 'org.apache.commons.collections4', shade_group + 'apache.commons.collections4' + relocate 'javax.annotation', shade_group + 'javax.annotation' + relocate 'gnu.trove', shade_group + 'gnu.trove' + relocate 'com.fasterxml', shade_group + 'fasterxml' + relocate 'club.minnced', shade_group + 'club.minnced' + relocate 'com.iwebpp', shade_group + 'iwebpp' + relocate 'com.jagrosh', shade_group + 'jagrosh' + relocate 'com.neovisionaries', shade_group + 'neovisionaries' + relocate 'me.hypherionmc.moonconfig', shade_group + 'moonconfig' + relocate 'me.hypherionmc.jqlite', shade_group + 'jqlite' + relocate 'net.dv8tion', shade_group + 'dv8tion' + relocate 'okhttp3', shade_group + 'okhttp3' + relocate 'okio', shade_group + 'okio' + relocate 'org.json', shade_group + 'json' + relocate 'org.sqlite', shade_group + 'sqlite' + relocate 'pw.chew', shade_group + 'chew' + relocate 'oshi', shade_group + 'oshi' + relocate 'kotlin', shade_group + 'kotlin' + relocate 'org.jasypt', shade_group + 'jasypt' + relocate 'com.google', shade_group + 'google' + relocate 'edu', shade_group + 'edu' + relocate 'io', shade_group + 'io' + relocate 'javassist', shade_group + 'javassist' + relocate 'net', shade_group + 'net' + relocate 'org.apache.commons.beanutils', shade_group + 'org.apache.commons.beanutils' + relocate 'org.apache.commons.collections', shade_group + 'org.apache.commons.collections' + relocate 'org.apache.commons.jxpath', shade_group + 'org.apache.commons.jxpath' + relocate 'org.apache.commons.logging', shade_group + 'org.apache.commons.logging' + relocate 'org.reflections', shade_group + 'org.reflections' + } + exclude 'META-INF/**' + setArchiveClassifier('') +} + +publishing { + publications { + mavenJava(MavenPublication) { + artifact(shadowJar) { + builtBy shadowJar + } + } + } + + repositories { + maven { + url System.getenv("MAVEN_URL") + credentials { + username System.getenv("MAVEN_USER") + password System.getenv("MAVEN_PASS") + } + } + } +} + +spotless { + java { + licenseHeaderFile(rootProject.file("HEADER")).yearSeparator("-") + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.encoding = 'UTF-8' + it.options.release = 16 +} + +tasks.withType(GenerateModuleMetadata) { + enabled = false +} + +build.finalizedBy(shadowJar) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..d4555da --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +version_major=0 +version_minor=0 +version_patch=2 + +shade_group=com.hypherionmc.sdlink.shaded. + +# Core Dependencies +jda=5.0.0-beta.9 +chewtils=2.0-SNAPSHOT +webhooks=0.7.5 +commons4=4.4 +oshi=5.8.5 +moonconfig=1.0.9 +jasypt=1.9.3 +json_db=1.0.106 + +# Optional Dependencies +log4j=2.17.2 +commons=3.12.0 +commonsio=2.11.0 +gson=2.10.1 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 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..9e756d2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat May 20 21:34:44 SAST 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 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. +# + +############################################################################## +# +# 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/master/subprojects/plugins/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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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"' + +# 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 + +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 + 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 + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + 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 + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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 + +@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=. +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%" == "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. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +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. + +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 %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..f610cb6 --- /dev/null +++ b/license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-2023 HypherionSA and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..7e975fc --- /dev/null +++ b/readme.md @@ -0,0 +1,21 @@ +### SDLink Core + +Minecraft independent code used by Simple Discord Link. This library contains all the core discord code, and code that is not +dependent on Minecraft. + +Requires JAVA 16 and Above! + +*** + +#### Building Instructions (For contributors) + +```gradle +// Build a jar +gradlew build + +// To Publish +gradlew publish + +// Publish to mavenLocal() +gradlew publishToMavenLocal +``` diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..17eaecf --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'sdlink-core' + diff --git a/src/main/java/com/hypherionmc/sdlink/core/accounts/DiscordAuthor.java b/src/main/java/com/hypherionmc/sdlink/core/accounts/DiscordAuthor.java new file mode 100644 index 0000000..23acaa9 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/accounts/DiscordAuthor.java @@ -0,0 +1,61 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.accounts; + +import com.hypherionmc.sdlink.core.services.SDLinkPlatform; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Represents a Message Author for messages sent from Minecraft to Discord + */ +public class DiscordAuthor { + + // User used for Server Messages + public static final DiscordAuthor SERVER = new DiscordAuthor(sdLinkConfig.channelsAndWebhooks.serverName, sdLinkConfig.channelsAndWebhooks.serverAvatar, true); + + private final String username; + private final String avatar; + private final boolean isServer; + + /** + * Internal. Use {@link #of(String, String)} + * @param username The Username of the Author + * @param avatar The avatar URL of the Author + * @param isServer Is the Author the Minecraft Server + */ + private DiscordAuthor(String username, String avatar, boolean isServer) { + this.username = username; + this.avatar = avatar; + this.isServer = isServer; + } + + /** + * Create a new Discord Author + * @param username The name/Username of the Author + * @param uuid The Mojang UUID of the Author + * @return A constructed {@link DiscordAuthor} + */ + public static DiscordAuthor of(String username, String uuid) { + return new DiscordAuthor( + username, + SDLinkPlatform.minecraftHelper.isOnlineMode() ? sdLinkConfig.chatConfig.playerAvatarType.resolve(uuid) : username, + false + ); + } + + public String getUsername() { + return username; + } + + public boolean isServer() { + return isServer; + } + + public String getAvatar() { + return avatar; + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/accounts/MinecraftAccount.java b/src/main/java/com/hypherionmc/sdlink/core/accounts/MinecraftAccount.java new file mode 100644 index 0000000..205f9cc --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/accounts/MinecraftAccount.java @@ -0,0 +1,344 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.accounts; + +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import com.hypherionmc.sdlink.core.discord.BotController; +import com.hypherionmc.sdlink.core.managers.RoleManager; +import com.hypherionmc.sdlink.core.messaging.Result; +import com.hypherionmc.sdlink.core.services.SDLinkPlatform; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.UserSnowflake; +import org.apache.commons.lang3.tuple.Pair; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; +import static com.hypherionmc.sdlink.core.managers.DatabaseManager.sdlinkDatabase; + +/** + * @author HypherionSA + * Represents a Minecraft Account. Used for communication between this library and minecraft + */ +public class MinecraftAccount { + + private final String username; + private final UUID uuid; + private final boolean isOffline; + private final boolean isValid; + + /** + * Internal. Use {@link #standard(String)} or {@link #offline(String)} + * @param username The Username of the Player + * @param uuid The UUID of the player + * @param isOffline Is this an OFFLINE/Unauthenticated Account + * @param isValid Is the account valid + */ + private MinecraftAccount(String username, UUID uuid, boolean isOffline, boolean isValid) { + this.username = username; + this.uuid = uuid; + this.isOffline = isOffline; + this.isValid = isValid; + } + + /** + * Tries to convert a Username to an online user account. If it can not, it will return an offline user + * @param username The username to search for + */ + public static MinecraftAccount standard(String username) { + Pair player = fetchPlayer(username); + + if (player.getRight() == null) { + return offline(username); + } + + return new MinecraftAccount( + player.getLeft(), + player.getRight(), + false, + player.getRight() != null + ); + } + + /** + * Convert a username to an offline account + * @param username The Username to search for + */ + public static MinecraftAccount offline(String username) { + Pair player = offlinePlayer(username); + return new MinecraftAccount( + player.getLeft(), + player.getRight(), + true, + true + ); + } + + public String getUsername() { + return username; + } + + public UUID getUuid() { + return uuid; + } + + public boolean isValid() { + return isValid; + } + + public boolean isOffline() { + return isOffline; + } + + /** + * Link a Minecraft account to a discord account + * @param member The discord user + * @param guild The server the command is run from + */ + public Result linkAccount(Member member, Guild guild) { + if (getStoredAccount() == null) { + return Result.error("We couldn't link your Minecraft and Discord Accounts together. If this error persists, please ask a staff member for help"); + } + + SDLinkAccount account = getStoredAccount(); + account.setDiscordID(member.getId()); + account.setAccountLinkCode(""); + + try { + sdlinkDatabase.upsert(account); + + String suffix = " [MC: " + this.username + "]"; + int availableChars = 32 - suffix.length(); + String nickname = member.getEffectiveName(); + + if (nickname.length() > availableChars) { + nickname = nickname.substring(0, availableChars - 3) + "..."; + } + + nickname += suffix; + + try { + member.modifyNickname(nickname).queue(); + + if (RoleManager.getLinkedRole() != null) { + guild.addRoleToMember(UserSnowflake.fromId(member.getId()), RoleManager.getLinkedRole()).queue(); + } + } catch (Exception e) { + if (sdLinkConfig.generalConfig.debugging) { + e.printStackTrace(); + } + } + + return Result.success("Your Discord and MC accounts have been linked"); + } catch (Exception e) { + e.printStackTrace(); + } + + return Result.error("Failed to complete account linking. Please inform the server owner"); + } + + /** + * Unlink a previously linked Discord and Minecraft Account + */ + public Result unlinkAccount() { + SDLinkAccount account = getStoredAccount(); + if (account == null) + return Result.error("No such account found in database"); + + try { + sdlinkDatabase.remove(account, SDLinkAccount.class); + return Result.success("Your discord and Minecraft accounts are no longer linked"); + } catch (Exception e) { + e.printStackTrace(); + } + + return Result.error("We could not unlink your discord and Minecraft accounts. Please inform the server owner"); + } + + /** + * Check if account database contains linking information + * and a valid discord user for this account + */ + public boolean isAccountLinked() { + SDLinkAccount account = getStoredAccount(); + + if (account == null) + return false; + + User discordUser = getDiscordUser(); + return discordUser != null; + } + + /** + * Whitelist a Player on Minecraft and store the info the database + * @param member The Discord Member that executed the command + * @param guild The Discord Server the command was executed in + */ + public Result whitelistAccount(Member member, Guild guild) { + if (getStoredAccount() == null) { + return Result.error("We couldn't link your Minecraft and Discord Accounts together. If this error persists, please ask a staff member for help"); + } + + SDLinkAccount account = getStoredAccount(); + account.setDiscordID(member.getId()); + account.setWhitelistCode(""); + + try { + if (!SDLinkPlatform.minecraftHelper.whitelistPlayer(MinecraftAccount.standard(account.getUsername())).isError()) { + account.setWhitelisted(true); + sdlinkDatabase.upsert(account); + + // Auto Linking is enabled, so we link the Discord and MC accounts + if (sdLinkConfig.whitelistingAndLinking.whitelisting.linkedWhitelist) { + this.linkAccount(member, guild); + } + } + + return Result.success("Your account has been whitelisted"); + } catch (Exception e) { + e.printStackTrace(); + } + + return Result.error("Failed to complete whitelisting. Please inform the server owner"); + } + + /** + * Remove a previously whitelisted account from Minecraft and the database + */ + public Result unwhitelistAccount() { + SDLinkAccount account = getStoredAccount(); + if (account == null) + return Result.error("No such account found in database"); + + try { + MinecraftAccount minecraftAccount = MinecraftAccount.standard(account.getUsername()); + Result whitelistResult = SDLinkPlatform.minecraftHelper.unWhitelistPlayer(minecraftAccount); + if (whitelistResult.isError()) { + return whitelistResult; + } else { + sdlinkDatabase.remove(account, SDLinkAccount.class); + + // Auto Linking is enabled. So we unlink the account + if (sdLinkConfig.whitelistingAndLinking.whitelisting.linkedWhitelist) { + this.unlinkAccount(); + } + + return Result.success("Your account has been removed from the whitelist"); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return Result.error("We could not unwhitelist your account. Please inform the server owner"); + } + + /** + * Check if the player is whitelisted on the MC server and if the database + * contains an entry for this player + */ + public boolean isAccountWhitelisted() { + SDLinkAccount account = getStoredAccount(); + + if (account == null) + return false; + + return !SDLinkPlatform.minecraftHelper.isPlayerWhitelisted(MinecraftAccount.standard(account.getUsername())).isError() && account.isWhitelisted(); + } + + /** + * Retrieve the stored account from the database + */ + public SDLinkAccount getStoredAccount() { + return sdlinkDatabase.findById(this.uuid.toString(), SDLinkAccount.class); + } + + /** + * Construct a new Database Entry for this account. + * Must only be used when a new entry is required + */ + public SDLinkAccount newDBEntry() { + SDLinkAccount account = new SDLinkAccount(); + account.setOffline(this.isOffline); + account.setUUID(this.uuid.toString()); + account.setWhitelisted(false); + account.setUsername(this.username); + + return account; + } + + /** + * Get the Discord Account name this player is linked to + */ + public String getDiscordName() { + SDLinkAccount storedAccount = sdlinkDatabase.findById(this.uuid, SDLinkAccount.class); + if (storedAccount == null || storedAccount.getDiscordID() == null || storedAccount.getDiscordID().isEmpty()) + return "Unlinked"; + + User discordUser = BotController.INSTANCE.getJDA().getUserById(storedAccount.getDiscordID()); + return discordUser == null ? "Unlinked" : discordUser.getName(); + } + + /** + * Get the Discord User this player is linked to + */ + public User getDiscordUser() { + SDLinkAccount storedAccount = sdlinkDatabase.findById(this.uuid, SDLinkAccount.class); + if (storedAccount == null || storedAccount.getDiscordID() == null || storedAccount.getDiscordID().isEmpty()) + return null; + + return BotController.INSTANCE.getJDA().getUserById(storedAccount.getDiscordID()); + } + + // Helper Methods + private static Pair fetchPlayer(String name) { + try { + BufferedReader read = new BufferedReader(new InputStreamReader(new URL("https://api.mojang.com/users/profiles/minecraft/" + name).openStream())); + JSONObject obj = new JSONObject(new JSONTokener(read)); + String uuid = ""; + String returnname = name; + + if (!obj.getString("name").isEmpty()) { + returnname = obj.getString("name"); + } + if (!obj.getString("id").isEmpty()) { + uuid = obj.getString("id"); + } + + read.close(); + return Pair.of(returnname, uuid.isEmpty() ? null : mojangIdToUUID(uuid)); + } catch (IOException | JSONException e) { + e.printStackTrace(); + } + return Pair.of("", null); + } + + private static UUID mojangIdToUUID(String id) { + final List strings = new ArrayList<>(); + strings.add(id.substring(0, 8)); + strings.add(id.substring(8, 12)); + strings.add(id.substring(12, 16)); + strings.add(id.substring(16, 20)); + strings.add(id.substring(20, 32)); + + return UUID.fromString(String.join("-", strings)); + } + + private static Pair offlinePlayer(String offlineName) { + return Pair.of(offlineName, UUID.nameUUIDFromBytes(("OfflinePlayer:" + offlineName).getBytes(StandardCharsets.UTF_8))); + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/AvatarType.java b/src/main/java/com/hypherionmc/sdlink/core/config/AvatarType.java new file mode 100644 index 0000000..569db52 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/AvatarType.java @@ -0,0 +1,31 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config; + +/** + * @author HypherionSA + * The type of User Icon/Avatar that will be used for Discord Messages + */ +public enum AvatarType { + AVATAR("https://mc-heads.net/avatar/{uuid}/512"), + HEAD("https://mc-heads.net/head/{uuid}/512"), + BODY("https://mc-heads.net/body/{uuid}"), + COMBO("https://mc-heads.net/combo/{uuid}/512"); + + private final String url; + + AvatarType(String url) { + this.url = url; + } + + @Override + public String toString() { + return this.url; + } + + public String resolve(String uuid) { + return this.url.replace("{uuid}", uuid); + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/ConfigController.java b/src/main/java/com/hypherionmc/sdlink/core/config/ConfigController.java new file mode 100644 index 0000000..2b98a1e --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/ConfigController.java @@ -0,0 +1,154 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config; + +import me.hypherionmc.moonconfig.core.CommentedConfig; +import me.hypherionmc.moonconfig.core.Config; +import me.hypherionmc.moonconfig.core.conversion.ObjectConverter; +import me.hypherionmc.moonconfig.core.file.CommentedFileConfig; +import com.hypherionmc.sdlink.core.util.EncryptionUtil; + +import java.io.File; + +/** + * @author HypherionSA + * Main Config class for Loading, Saving and Upgrading configs + */ +public class ConfigController { + + // Private internal variables + private final File configFile; + public static int configVer = 1; + + // Instance of the currently loaded config + public static SDLinkConfig sdLinkConfig; + + public ConfigController() { + File path = new File("config/"); + if (!path.exists()) + path.mkdirs(); + + this.configFile = new File(path.getAbsolutePath() + File.separator + "simple-discord-link.toml"); + initConfig(); + } + + /** + * Set up the Config File as needed. + * This will either Create, Upgrade or load an existing config file + */ + private void initConfig() { + Config.setInsertionOrderPreserved(true); + if (!configFile.exists() || configFile.length() < 10) { + SDLinkConfig config = new SDLinkConfig(); + saveConfig(config); + performEncryption(); + } else { + configUpgrade(); + performEncryption(); + } + loadConfig(); + } + + /** + * Serialize an existing config file into an instance of {@link SDLinkConfig} + */ + private void loadConfig() { + ObjectConverter converter = new ObjectConverter(); + CommentedFileConfig config = CommentedFileConfig.builder(configFile).build(); + config.load(); + sdLinkConfig = converter.toObject(config, SDLinkConfig::new); + config.close(); + } + + /** + * Serialize an instance of {@link SDLinkConfig} to the config file + * @param conf An instance of the config to save + */ + public void saveConfig(Object conf) { + ObjectConverter converter = new ObjectConverter(); + CommentedFileConfig config = CommentedFileConfig.builder(configFile).build(); + + converter.toConfig(conf, config); + config.save(); + config.close(); + } + + /** + * Handle config structure changes between version changes + */ + private void configUpgrade() { + CommentedFileConfig oldConfig = CommentedFileConfig.builder(configFile).build(); + CommentedFileConfig newConfig = CommentedFileConfig.builder(configFile).build(); + + newConfig.load(); + newConfig.clear(); + oldConfig.load(); + + if (oldConfig.getInt("general.configVersion") == configVer) { + newConfig.close(); + oldConfig.close(); + return; + } + + ObjectConverter objectConverter = new ObjectConverter(); + objectConverter.toConfig(new SDLinkConfig(), newConfig); + + oldConfig.valueMap().forEach((key, value) -> { + if (value instanceof CommentedConfig commentedConfig) { + commentedConfig.valueMap().forEach((subKey, subValue) -> { + if (newConfig.contains(key + "." + subKey)) { + newConfig.set(key + "." + subKey, subValue); + } + }); + } else { + if (newConfig.contains(key)) { + newConfig.set(key, value); + } + } + }); + + configFile.renameTo(new File(configFile.getAbsolutePath().replace(".toml", ".old"))); + newConfig.set("general.configVersion", configVer); + newConfig.save(); + newConfig.close(); + oldConfig.close(); + } + + /** + * Apply encryption to Bot-Token and Webhook URLS + */ + private void performEncryption() { + CommentedFileConfig oldConfig = CommentedFileConfig.builder(configFile).build(); + oldConfig.load(); + + String botToken = oldConfig.getOrElse("botConfig.botToken", ""); + String chatWebhook = oldConfig.getOrElse("channelsAndWebhooks.webhooks.chatWebhook", ""); + String eventsWebhook = oldConfig.getOrElse("channelsAndWebhooks.webhooks.eventsWebhook", ""); + String consoleWebhook = oldConfig.getOrElse("channelsAndWebhooks.webhooks.consoleWebhook", ""); + + if (!botToken.isEmpty()) { + botToken = EncryptionUtil.INSTANCE.encrypt(botToken); + oldConfig.set("botConfig.botToken", botToken); + } + + if (!chatWebhook.isEmpty()) { + chatWebhook = EncryptionUtil.INSTANCE.encrypt(chatWebhook); + oldConfig.set("channelsAndWebhooks.webhooks.chatWebhook", chatWebhook); + } + + if (!eventsWebhook.isEmpty()) { + eventsWebhook = EncryptionUtil.INSTANCE.encrypt(eventsWebhook); + oldConfig.set("channelsAndWebhooks.webhooks.eventsWebhook", eventsWebhook); + } + + if (!consoleWebhook.isEmpty()) { + consoleWebhook = EncryptionUtil.INSTANCE.encrypt(consoleWebhook); + oldConfig.set("channelsAndWebhooks.webhooks.consoleWebhook", consoleWebhook); + } + + oldConfig.save(); + oldConfig.close(); + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/SDLinkConfig.java b/src/main/java/com/hypherionmc/sdlink/core/config/SDLinkConfig.java new file mode 100644 index 0000000..57331a1 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/SDLinkConfig.java @@ -0,0 +1,52 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; +import com.hypherionmc.sdlink.core.config.impl.*; + +/** + * @author HypherionSA + * The main mod config Structure + */ +public class SDLinkConfig { + + @Path("general") + @SpecComment("General Mod Config") + public GeneralConfigSettings generalConfig = new GeneralConfigSettings(); + + @Path("botConfig") + @SpecComment("Config specific to the discord bot") + public BotConfigSettings botConfig = new BotConfigSettings(); + + @Path("channelsAndWebhooks") + @SpecComment("Config relating to the discord channels and webhooks to use with the mod") + public ChannelWebhookConfig channelsAndWebhooks = new ChannelWebhookConfig(); + + @Path("chat") + @SpecComment("Configure which types of messages are delivered to Minecraft/Discord") + public ChatSettingsConfig chatConfig = new ChatSettingsConfig(); + + @Path("messageFormatting") + @SpecComment("Change the format in which messages are displayed") + public MessageFormatting messageFormatting = new MessageFormatting(); + + @Path("messageDestinations") + @SpecComment("Change in which channel messages appear") + public MessageChannelConfig messageDestinations = new MessageChannelConfig(); + + @Path("whitelistingAndLinking") + @SpecComment("Configure Whitelisting and Account Linking through the bot") + public LinkAndWhitelistConfigSettings whitelistingAndLinking = new LinkAndWhitelistConfigSettings(); + + @Path("botCommands") + @SpecComment("Enable or Disable certain bot commands") + public BotCommandsConfig botCommands = new BotCommandsConfig(); + + @Path("linkedCommands") + @SpecComment("Execute Minecraft commands in Discord") + public LinkedCommandsConfig linkedCommands = new LinkedCommandsConfig(); +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/impl/BotCommandsConfig.java b/src/main/java/com/hypherionmc/sdlink/core/config/impl/BotCommandsConfig.java new file mode 100644 index 0000000..df2cc40 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/impl/BotCommandsConfig.java @@ -0,0 +1,28 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config.impl; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; + +/** + * @author HypherionSA + * Config Structure to allow disabling some bot commands + */ +public class BotCommandsConfig { + + @Path("allowPlayerList") + @SpecComment("Enable/Disable the Player List command") + public boolean allowPlayerList = true; + + @Path("allowServerStatus") + @SpecComment("Enable/Disable the Server Status command") + public boolean allowServerStatus = true; + + @Path("allowHelpCommand") + @SpecComment("Enable/Disable the Help command") + public boolean allowHelpCommand = true; + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/impl/BotConfigSettings.java b/src/main/java/com/hypherionmc/sdlink/core/config/impl/BotConfigSettings.java new file mode 100644 index 0000000..8604567 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/impl/BotConfigSettings.java @@ -0,0 +1,75 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config.impl; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; +import net.dv8tion.jda.api.entities.Activity; + +/** + * @author HypherionSA + * Config Structure for the Core bot settings + */ +public class BotConfigSettings { + + @Path("botToken") + @SpecComment("The token of the Discord Bot to use. This will be encrypted on first load. See https://sdlink.fdd-docs.com/initial-setup/ to find this") + public String botToken = ""; + + @Path("statusUpdateInterval") + @SpecComment("How often the Bot Status will update on Discord (in Seconds)") + public int statusUpdateInterval = 30; + + @Path("staffRole") + @SpecComment("Role Name or ID that is allowed to use Staff Functions. If not defined, it will default back to Admin/Kick Perms") + public String staffRole = ""; + + @Path("botStatus") + @SpecComment("Control what the Discord Bot will display as it's status message") + public BotStatus botStatus = new BotStatus(); + + @Path("topicUpdates") + @SpecComment("Define how the bot should handle channel topic updates on the chat channel") + public ChannelTopic channelTopic = new ChannelTopic(); + + @Path("invite") + @SpecComment("Configure the in-game Discord Invite command") + public DiscordInvite invite = new DiscordInvite(); + + public static class BotStatus { + @Path("status") + @SpecComment("Do not add Playing. A status to display on the bot. You can use %players% and %maxplayers% to show the number of players on the server") + public String botStatus = "Minecraft"; + + @Path("botStatusType") + @SpecComment("The type of the status displayed on the bot. Valid entries are: PLAYING, STREAMING, WATCHING, LISTENING") + public Activity.ActivityType botStatusType = Activity.ActivityType.PLAYING; + + @Path("botStatusStreamingURL") + @SpecComment("The URL that will be used when the \"botStatusType\" is set to \"STREAMING\", required to display as \"streaming\".") + public String botStatusStreamingURL = "https://twitch.tv/twitch"; + } + + public static class ChannelTopic { + @Path("doTopicUpdates") + @SpecComment("Should the bot update the topic of your chat channel automatically every 6 Minutes") + public boolean doTopicUpdates = true; + + @Path("channelTopic") + @SpecComment("A topic for the Chat Relay channel. You can use %player%, %maxplayers%, %uptime% or just leave it empty.") + public String channelTopic = "Playing Minecraft with %players%/%maxplayers% people | Uptime: %uptime%"; + } + + public static class DiscordInvite { + @Path("inviteLink") + @SpecComment("If this is defined, it will enable the in-game Discord command") + public String inviteLink = ""; + + @Path("inviteMessage") + @SpecComment("The message to show when someone uses /discord command. You can use %inviteurl%") + public String inviteMessage = "Hey, check out our discord server here -> %inviteurl%"; + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/impl/ChannelWebhookConfig.java b/src/main/java/com/hypherionmc/sdlink/core/config/impl/ChannelWebhookConfig.java new file mode 100644 index 0000000..4214217 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/impl/ChannelWebhookConfig.java @@ -0,0 +1,64 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config.impl; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; + +/** + * @author HypherionSA + * Config Structure to control Channels and Webhooks used by the bot + */ +public class ChannelWebhookConfig { + + @Path("serverAvatar") + @SpecComment("A DIRECT link to an image to use as the avatar for server messages. Also used for embeds") + public String serverAvatar = ""; + + @Path("serverName") + @SpecComment("The name to display for Server messages when using Webhooks") + public String serverName = "Minecraft Server"; + + @Path("channels") + @SpecComment("Config relating to the discord channels to use with the mod") + public Channels channels = new Channels(); + + @Path("webhooks") + @SpecComment("Config relating to the discord Webhooks to use with the mod") + public Webhooks webhooks = new Webhooks(); + + public static class Channels { + @Path("chatChannelID") + @SpecComment("REQUIRED! The ID of the channel to post in and relay messages from. This is still needed, even in webhook mode") + public long chatChannelID = 0; + + @Path("eventsChannelID") + @SpecComment("If this ID is set, event messages will be posted in this channel instead of the chat channel") + public long eventsChannelID = 0; + + @Path("consoleChannelID") + @SpecComment("If this ID is set, console messages sent after the bot started will be relayed here, and you can execute minecraft commands here") + public long consoleChannelID = 0; + } + + public static class Webhooks { + @Path("enabled") + @SpecComment("Prefer Webhook Messages over Standard Bot Messages") + public boolean enabled = false; + + @Path("chatWebhook") + @SpecComment("The URL of the channel webhook to use for Chat Messages. Will be encrypted on first run") + public String chatWebhook = ""; + + @Path("eventsWebhook") + @SpecComment("The URL of the channel webhook to use for Server Messages Will be encrypted on first run") + public String eventsWebhook = ""; + + @Path("consoleWebhook") + @SpecComment("The URL of the channel webhook to use for Console Messages Will be encrypted on first run") + public String consoleWebhook = ""; + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/impl/ChatSettingsConfig.java b/src/main/java/com/hypherionmc/sdlink/core/config/impl/ChatSettingsConfig.java new file mode 100644 index 0000000..069424c --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/impl/ChatSettingsConfig.java @@ -0,0 +1,92 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config.impl; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; +import com.hypherionmc.sdlink.core.config.AvatarType; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author HypherionSA + * Config Structure to control what types of messages are supported by the mod + */ +public class ChatSettingsConfig { + + @Path("formatting") + @SpecComment("Convert Discord to MC, and MC to Discord Formatting") + public boolean formatting = true; + + @Path("sendConsoleMessages") + @SpecComment("Should console messages be sent to the Console Channel") + public boolean sendConsoleMessages = false; + + @Path("playerAvatarType") + @SpecComment("The type of image to use as the player icon in messages. Valid entries are: AVATAR, HEAD, BODY, COMBO") + public AvatarType playerAvatarType = AvatarType.HEAD; + + @Path("relayTellRaw") + @SpecComment("Should messages sent with TellRaw be sent to discord as a chat? (Experimental)") + public boolean relayTellRaw = true; + + @Path("relayFullCommands") + @SpecComment("Should the entire command executed be relayed to discord, or only the name of the command") + public boolean relayFullCommands = false; + + @Path("ignoreBots") + @SpecComment("Should messages from bots be relayed") + public boolean ignoreBots = true; + + @Path("serverStarting") + @SpecComment("Should SERVER STARTING messages be shown") + public boolean serverStarting = true; + + @Path("serverStarted") + @SpecComment("Should SERVER STARTED messages be shown") + public boolean serverStarted = true; + + @Path("serverStopping") + @SpecComment("Should SERVER STOPPING messages be shown") + public boolean serverStopping = true; + + @Path("serverStopped") + @SpecComment("Should SERVER STOPPED messages be shown") + public boolean serverStopped = true; + + @Path("playerMessages") + @SpecComment("Should the chat be relayed") + public boolean playerMessages = true; + + @Path("playerJoin") + @SpecComment("Should Player Join messages be posted") + public boolean playerJoin = true; + + @Path("playerLeave") + @SpecComment("Should Player Leave messages be posted") + public boolean playerLeave = true; + + @Path("advancementMessages") + @SpecComment("Should Advancement messages be posted") + public boolean advancementMessages = true; + + @Path("deathMessages") + @SpecComment("Should Death Announcements be posted") + public boolean deathMessages = true; + + @Path("sendSayCommand") + @SpecComment("Should Messages from the /say command be posted") + public boolean sendSayCommand = true; + + @Path("broadcastCommands") + @SpecComment("Should commands be posted to discord") + public boolean broadcastCommands = true; + + @Path("ignoredCommands") + @SpecComment("Commands that should not be broadcasted to discord") + public List ignoredCommands = new ArrayList() {{ add("particle"); add("login"); add("execute"); }}; + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/impl/GeneralConfigSettings.java b/src/main/java/com/hypherionmc/sdlink/core/config/impl/GeneralConfigSettings.java new file mode 100644 index 0000000..56b925a --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/impl/GeneralConfigSettings.java @@ -0,0 +1,28 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config.impl; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; +import com.hypherionmc.sdlink.core.config.ConfigController; + +/** + * @author HypherionSA + * General Mod Settings config Structure + */ +public class GeneralConfigSettings { + + @Path("enabled") + @SpecComment("Should the mod be enabled or not") + public boolean enabled = true; + + @Path("debugging") + @SpecComment("Enable Additional Logging. Used for Fault Finding. WARNING: CAUSES LOG SPAM!") + public boolean debugging = false; + + @Path("configVersion") + @SpecComment("Internal version control. DO NOT TOUCH!") + public int configVersion = ConfigController.configVer; +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/impl/LinkAndWhitelistConfigSettings.java b/src/main/java/com/hypherionmc/sdlink/core/config/impl/LinkAndWhitelistConfigSettings.java new file mode 100644 index 0000000..aa32f5f --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/impl/LinkAndWhitelistConfigSettings.java @@ -0,0 +1,55 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config.impl; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; + +/** + * @author HypherionSA + * Config Structure to control Whitelisting and Account Linking + */ +public class LinkAndWhitelistConfigSettings { + + @Path("whiteListing") + @SpecComment("Control how the bot handles Whitelisting Players, if at all") + public Whitelisting whitelisting = new Whitelisting(); + + @Path("accountLinking") + @SpecComment("Control how the bot handles Discord -> MC Account Linking, if at all") + public AccountLinking accountLinking = new AccountLinking(); + + public static class AccountLinking { + @Path("accountlinking") + @SpecComment("Allow users to Link their MC and Discord accounts") + public boolean accountLinking = false; + + @Path("linkedRole") + @SpecComment("If a role ID (or name) is defined here, it will be assigned to players when their MC and Discord accounts are linked") + public String linkedRole = ""; + + @Path("requireLinking") + @SpecComment("Require users to link their Discord and Minecraft accounts before joining the server") + public boolean requireLinking = false; + } + + public static class Whitelisting { + @Path("whitelisting") + @SpecComment("Should the bot be allowed to whitelist/un-whitelist players.") + public boolean whitelisting = false; + + @Path("linkedWhitelist") + @SpecComment("Automatically link Minecraft and Discord Accounts when a user is whitelisted") + public boolean linkedWhitelist = false; + + @Path("staffOnlyWhitelist") + @SpecComment("Should only staff be allowed to whitelist players") + public boolean staffOnlyWhitelist = false; + + @Path("autoWhitelistRole") + @SpecComment("If a role ID (or name) is defined here, it will be assigned to players when they are whitelisted") + public String autoWhitelistRole = ""; + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/impl/LinkedCommandsConfig.java b/src/main/java/com/hypherionmc/sdlink/core/config/impl/LinkedCommandsConfig.java new file mode 100644 index 0000000..247f8df --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/impl/LinkedCommandsConfig.java @@ -0,0 +1,41 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config.impl; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author HypherionSA + * Main Config Structure to control Discord -> MC Commands + */ +public class LinkedCommandsConfig { + + @Path("enabled") + @SpecComment("Should linked commands be enabled") + public boolean enabled = false; + + @Path("commands") + @SpecComment("Commands to be linked") + public List commands = new ArrayList<>(); + + public static class Command { + @Path("mcCommand") + @SpecComment("The Minecraft Command. Use %args% and %args(1-9)% (for example %args1%) to pass everything after the discordCommand to Minecraft") + public String mcCommand; + + @Path("discordCommand") + @SpecComment("The command slug in discord. To be used as /mc slug or ~mc slug") + public String discordCommand; + + @Path("discordRole") + @SpecComment("Discord Role Name of ID of the role that is allowed to execute this command. If empty, all players will be allowed to use this command") + public String discordRole; + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/impl/MessageChannelConfig.java b/src/main/java/com/hypherionmc/sdlink/core/config/impl/MessageChannelConfig.java new file mode 100644 index 0000000..782575c --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/impl/MessageChannelConfig.java @@ -0,0 +1,59 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config.impl; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; +import com.hypherionmc.sdlink.core.messaging.MessageDestination; + +/** + * @author HypherionSA + * Config Structure to control the destinations of messages + */ +public class MessageChannelConfig { + + @Path("chat") + @SpecComment("Control where CHAT messages are delivered") + public DestinationObject chat = DestinationObject.of(MessageDestination.CHAT, false); + + @Path("startStop") + @SpecComment("Control where START/STOP messages are delivered") + public DestinationObject startStop = DestinationObject.of(MessageDestination.EVENT, false); + + @Path("joinLeave") + @SpecComment("Control where JOIN/LEAVE messages are delivered") + public DestinationObject joinLeave = DestinationObject.of(MessageDestination.EVENT, false); + + @Path("advancements") + @SpecComment("Control where ADVANCEMENT messages are delivered") + public DestinationObject advancements = DestinationObject.of(MessageDestination.EVENT, false); + + @Path("death") + @SpecComment("Control where DEATH messages are delivered") + public DestinationObject death = DestinationObject.of(MessageDestination.EVENT, false); + + @Path("commands") + @SpecComment("Control where COMMAND messages are delivered") + public DestinationObject commands = DestinationObject.of(MessageDestination.EVENT, false); + + public static class DestinationObject { + @Path("channel") + @SpecComment("The Channel the message will be delivered to. Valid entries are CHAT, EVENT, CONSOLE") + public MessageDestination channel; + + @Path("useEmbed") + @SpecComment("Should the message be sent using EMBED style messages") + public boolean useEmbed; + + DestinationObject(MessageDestination destination, boolean useEmbed) { + this.channel = destination; + this.useEmbed = useEmbed; + } + + public static DestinationObject of(MessageDestination destination, boolean useEmbed) { + return new DestinationObject(destination, useEmbed); + } + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/config/impl/MessageFormatting.java b/src/main/java/com/hypherionmc/sdlink/core/config/impl/MessageFormatting.java new file mode 100644 index 0000000..1086d70 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/config/impl/MessageFormatting.java @@ -0,0 +1,55 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.config.impl; + +import me.hypherionmc.moonconfig.core.conversion.Path; +import me.hypherionmc.moonconfig.core.conversion.SpecComment; + +/** + * @author HypherionSA + * Config Structure to control Discord/MC Message Formatting + */ +public class MessageFormatting { + + @Path("mcPrefix") + @SpecComment("Prefix to add to Minecraft when a message is relayed from Discord. Supports MC formatting. Use %user% for the Discord Username") + public String mcPrefix = "\u00A7e[Discord]\u00A7r %user%: "; + + @Path("serverStarting") + @SpecComment("Server Starting Message") + public String serverStarting = "*Server is starting...*"; + + @Path("serverStarted") + @SpecComment("Server Started Message") + public String serverStarted = "*Server has started. Enjoy!*"; + + @Path("serverStopping") + @SpecComment("Server Stopping Message") + public String serverStopping = "*Server is stopping...*"; + + @Path("serverStopped") + @SpecComment("Server Stopped Message") + public String serverStopped = "*Server has stopped...*"; + + @Path("playerJoined") + @SpecComment("Player Joined Message. Use %player% to display the player name") + public String playerJoined = "*%player% has joined the server!*"; + + @Path("playerLeft") + @SpecComment("Player Left Message. Use %player% to display the player name") + public String playerLeft = "*%player% has left the server!*"; + + @Path("achievements") + @SpecComment("Achievement Messages. Available variables: %player%, %title%, %description%") + public String achievements = "*%player% has made the advancement [%title%]: %description%*"; + + @Path("chat") + @SpecComment("Chat Messages. THIS DOES NOT APPLY TO EMBED OR WEBHOOK MESSAGES. Available variables: %player%, %message%") + public String chat = "%player%: %message%"; + + @Path("commands") + @SpecComment("Command Messages. Available variables: %player%, %command%") + public String commands = "%player% **executed command**: *%command%*"; +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/database/SDLinkAccount.java b/src/main/java/com/hypherionmc/sdlink/core/database/SDLinkAccount.java new file mode 100644 index 0000000..1e4f97c --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/database/SDLinkAccount.java @@ -0,0 +1,102 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.database; + +import io.jsondb.annotation.Document; +import io.jsondb.annotation.Id; + +/** + * @author HypherionSA + * JSON based database to hold accounts the bot has interacted with. + * This is used for Account Linking and Whitelisting + */ +@Document(collection = "accounts", schemaVersion = "1.0") +public class SDLinkAccount { + @Id + private String UUID; + private String username; + private String addedBy; + private String discordID; + private String accountLinkCode; + private String whitelistCode; + private boolean isWhitelisted; + private boolean isOffline; + + public String getAccountLinkCode() { + if (accountLinkCode == null) + return ""; + return accountLinkCode; + } + + public boolean isOffline() { + return isOffline; + } + + public boolean isWhitelisted() { + return isWhitelisted; + } + + public String getDiscordID() { + if (discordID == null) + return ""; + return discordID; + } + + public String getAddedBy() { + if (addedBy == null) + return ""; + return addedBy; + } + + public String getUsername() { + if (username == null) + return ""; + return username; + } + + public String getUUID() { + if (UUID == null) + return ""; + return UUID; + } + + public String getWhitelistCode() { + if (whitelistCode == null) + return ""; + return whitelistCode; + } + + public void setAccountLinkCode(String accountLinkCode) { + this.accountLinkCode = accountLinkCode; + } + + public void setAddedBy(String addedBy) { + this.addedBy = addedBy; + } + + public void setDiscordID(String discordID) { + this.discordID = discordID; + } + + public void setOffline(boolean offline) { + isOffline = offline; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setUUID(String UUID) { + this.UUID = UUID; + } + + public void setWhitelistCode(String whitelistCode) { + this.whitelistCode = whitelistCode; + } + + public void setWhitelisted(boolean whitelisted) { + isWhitelisted = whitelisted; + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/BotController.java b/src/main/java/com/hypherionmc/sdlink/core/discord/BotController.java new file mode 100644 index 0000000..d9ec790 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/BotController.java @@ -0,0 +1,195 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord; + +import com.hypherionmc.sdlink.core.config.ConfigController; +import com.hypherionmc.sdlink.core.discord.commands.CommandManager; +import com.hypherionmc.sdlink.core.discord.events.DiscordEventHandler; +import com.hypherionmc.sdlink.core.managers.DatabaseManager; +import com.hypherionmc.sdlink.core.managers.WebhookManager; +import com.hypherionmc.sdlink.core.services.SDLinkPlatform; +import com.hypherionmc.sdlink.core.util.EncryptionUtil; +import com.hypherionmc.sdlink.core.util.ThreadedEventManager; +import com.jagrosh.jdautilities.command.CommandClient; +import com.jagrosh.jdautilities.command.CommandClientBuilder; +import com.jagrosh.jdautilities.commons.waiter.EventWaiter; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.ChunkingFilter; +import net.dv8tion.jda.api.utils.MemberCachePolicy; +import org.slf4j.Logger; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * The main Discord Bot class. This controls everything surrounding the bot itself + */ +public class BotController { + + // Public instance of this class that can be called anywhere + public static BotController INSTANCE; + + // Thread Execution Manager + public static final ScheduledExecutorService taskManager = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); + + // Required Variables + private JDA _jda; + private final EventWaiter eventWaiter = new EventWaiter(); + private final Logger logger; + + /** + * Construct a new instance of this class + * @param logger A constructed {@link Logger} that the bot will use + */ + public static void newInstance(Logger logger) { + if (INSTANCE != null) { + INSTANCE.shutdownBot(false); + } + new BotController(logger); + } + + /** + * INTERNAL + * @param logger A constructed {@link Logger} that the bot will use + */ + private BotController(Logger logger) { + INSTANCE = this; + this.logger = logger; + + new ConfigController(); + + DatabaseManager.initialize(); + + // Initialize Webhook Clients + WebhookManager.init(); + } + + /** + * Start the bot and handle all the startup work + */ + public void initializeBot() { + if (sdLinkConfig == null) { + logger.error("Failed to load config. Check your log for errors"); + return; + } + + if (sdLinkConfig.botConfig.botToken.isEmpty()) { + logger.error("Missing bot token. Mod will be disabled"); + return; + } + + if (!sdLinkConfig.generalConfig.enabled) + return; + + try { + String token = EncryptionUtil.INSTANCE.decrypt(sdLinkConfig.botConfig.botToken); + _jda = JDABuilder.createLight( + token, + GatewayIntent.GUILD_MEMBERS, + GatewayIntent.GUILD_MESSAGES, + GatewayIntent.MESSAGE_CONTENT, + GatewayIntent.GUILD_MESSAGE_REACTIONS + ) + .setMemberCachePolicy(MemberCachePolicy.ALL) + .setChunkingFilter(ChunkingFilter.ALL) + .setBulkDeleteSplittingEnabled(true) + .setEventManager(new ThreadedEventManager()) + .build(); + + // Setup Commands + CommandClientBuilder clientBuilder = new CommandClientBuilder(); + clientBuilder.setOwnerId("354707828298088459"); + clientBuilder.setHelpWord("help"); + clientBuilder.useHelpBuilder(false); + //clientBuilder.forceGuildOnly(750990873311051786L); + + CommandClient commandClient = clientBuilder.build(); + CommandManager.INSTANCE.register(commandClient); + + // Register Event Handlers + _jda.addEventListener(commandClient, eventWaiter, new DiscordEventHandler()); + _jda.setAutoReconnect(true); + + } catch (Exception e) { + logger.error("Failed to connect to discord", e); + } + } + + /** + * Check if the bot is in a state to send messages to discord + */ + public boolean isBotReady() { + if (sdLinkConfig == null) + return false; + + if (!sdLinkConfig.generalConfig.enabled) + return false; + + if (_jda == null) + return false; + + if (_jda.getStatus() == JDA.Status.SHUTTING_DOWN || _jda.getStatus() == JDA.Status.SHUTDOWN) + return false; + + return _jda.getStatus() == JDA.Status.CONNECTED; + } + + /** + * Shutdown the Bot, without forcing a shutdown + */ + public void shutdownBot() { + this.shutdownBot(true); + } + + /** + * Shutdown the Bot, optionally forcing a shutdown + * @param forced Should the shutdown be forced + */ + public void shutdownBot(boolean forced) { + if (_jda != null) { + _jda.shutdown(); + } + + WebhookManager.shutdown(); + + if (forced) { + // Workaround for Bot thread hanging after server shutdown + taskManager.schedule(() -> { + taskManager.shutdownNow(); + System.exit(1); + }, 10, TimeUnit.SECONDS); + } + } + + /** + * Ensure that whitelisting is set up properly, so the bot can use the feature + */ + public void checkWhiteListing() { + if (!sdLinkConfig.whitelistingAndLinking.whitelisting.whitelisting) + return; + + if (SDLinkPlatform.minecraftHelper.checkWhitelisting().isError()) { + getLogger().error("SDLink Whitelisting is enabled, but server side whitelisting is disabled"); + } + } + + public Logger getLogger() { + return logger; + } + + public JDA getJDA() { + return this._jda; + } + + public EventWaiter getEventWaiter() { + return eventWaiter; + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/CommandManager.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/CommandManager.java new file mode 100644 index 0000000..8accc06 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/CommandManager.java @@ -0,0 +1,84 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands; + +import com.hypherionmc.sdlink.core.discord.commands.slash.general.HelpSlashCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.general.PlayerListSlashCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.general.ServerStatusSlashCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.linking.ConfirmAccountLinkSlashCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.linking.LinkAccountCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.linking.UnlinkAccountSlashCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.linking.ViewLinkedAccountsCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.whitelist.ConfirmWhitelistSlashCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.whitelist.UnWhitelistAccountSlashCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.whitelist.ViewWhitelistedAccountsSlashCommand; +import com.hypherionmc.sdlink.core.discord.commands.slash.whitelist.WhitelistAccountCommand; +import com.jagrosh.jdautilities.command.CommandClient; +import com.jagrosh.jdautilities.command.SlashCommand; + +import java.util.HashSet; +import java.util.Set; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Command Manager class to control how commands are registered to discord + */ +public class CommandManager { + + public static final CommandManager INSTANCE = new CommandManager(); + + private final Set commands = new HashSet<>(); + + private CommandManager() { + this.addCommands(); + } + + private void addCommands() { + // Register Account Linking commands, if linking is enabled + if (sdLinkConfig.whitelistingAndLinking.accountLinking.accountLinking) { + commands.add(new LinkAccountCommand()); + commands.add(new ConfirmAccountLinkSlashCommand()); + commands.add(new UnlinkAccountSlashCommand()); + commands.add(new ViewLinkedAccountsCommand()); + } + + // Register Whitelist commands, if whitelisting is enabled + if (sdLinkConfig.whitelistingAndLinking.whitelisting.whitelisting) { + commands.add(new WhitelistAccountCommand()); + commands.add(new ConfirmWhitelistSlashCommand()); + commands.add(new ViewWhitelistedAccountsSlashCommand()); + commands.add(new UnWhitelistAccountSlashCommand()); + } + + // Enable the Server Status command + if (sdLinkConfig.botCommands.allowServerStatus) { + commands.add(new ServerStatusSlashCommand()); + } + + // Enable the Player List command + if (sdLinkConfig.botCommands.allowPlayerList) { + commands.add(new PlayerListSlashCommand()); + } + + // Enable the Help command + if (sdLinkConfig.botCommands.allowHelpCommand) { + commands.add(new HelpSlashCommand()); + } + } + + /** + * INTERNAL. Used to register slash commands + * @param client The Discord Command Client instance + */ + public void register(CommandClient client) { + commands.forEach(client::addSlashCommand); + } + + public Set getCommands() { + return commands; + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/LINKEDSLASHCOMMANDS b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/LINKEDSLASHCOMMANDS new file mode 100644 index 0000000..58063e3 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/LINKEDSLASHCOMMANDS @@ -0,0 +1 @@ +// TODO Don't Forget This \ No newline at end of file diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/SDLinkSlashCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/SDLinkSlashCommand.java new file mode 100644 index 0000000..3a8a70f --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/SDLinkSlashCommand.java @@ -0,0 +1,28 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash; + +import com.hypherionmc.sdlink.core.managers.RoleManager; +import com.jagrosh.jdautilities.command.SlashCommand; +import net.dv8tion.jda.api.Permission; + +/** + * @author HypherionSA + * Extention of {@link SlashCommand} to implement our Permission handling + */ +public abstract class SDLinkSlashCommand extends SlashCommand { + + public SDLinkSlashCommand(boolean requiresPerms) { + if (requiresPerms) { + if (RoleManager.getStaffRole() != null) { + this.requiredRole = RoleManager.getStaffRole().getName(); + } else { + this.userPermissions = new Permission[] { Permission.ADMINISTRATOR, Permission.KICK_MEMBERS }; + } + } + this.guildOnly = true; + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/general/HelpSlashCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/general/HelpSlashCommand.java new file mode 100644 index 0000000..e3a93ad --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/general/HelpSlashCommand.java @@ -0,0 +1,39 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.general; + +import com.hypherionmc.sdlink.core.discord.commands.CommandManager; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.jagrosh.jdautilities.command.SlashCommand; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.dv8tion.jda.api.EmbedBuilder; + +import java.awt.*; +import java.util.Set; + +/** + * @author HypherionSA + * The Help Command for the bot + */ +public class HelpSlashCommand extends SDLinkSlashCommand { + + public HelpSlashCommand() { + super(false); + this.name = "help"; + this.help = "Bot commands and help"; + } + + @Override + protected void execute(SlashCommandEvent event) { + Set commands = CommandManager.INSTANCE.getCommands(); + + EmbedBuilder builder = new EmbedBuilder(); + builder.setTitle("Bot commands"); + builder.setColor(Color.BLUE); + + commands.forEach(cmd -> builder.addField(cmd.getName(), cmd.getHelp(), false)); + event.replyEmbeds(builder.build()).setEphemeral(true).queue(); + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/general/PlayerListSlashCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/general/PlayerListSlashCommand.java new file mode 100644 index 0000000..d0ff652 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/general/PlayerListSlashCommand.java @@ -0,0 +1,82 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.general; + +import com.hypherionmc.sdlink.core.accounts.MinecraftAccount; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.services.SDLinkPlatform; +import com.hypherionmc.sdlink.core.util.MessageUtil; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import com.jagrosh.jdautilities.menu.EmbedPaginator; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author HypherionSA + * Command to view a list of online players currently on the server + */ +public class PlayerListSlashCommand extends SDLinkSlashCommand { + + public PlayerListSlashCommand() { + super(false); + + this.name = "playerlist"; + this.help = "List currently online players on the server"; + this.guildOnly = true; + } + + @Override + protected void execute(SlashCommandEvent event) { + List players = SDLinkPlatform.minecraftHelper.getOnlinePlayers(); + + EmbedBuilder builder = new EmbedBuilder(); + List pages = new ArrayList<>(); + AtomicInteger count = new AtomicInteger(); + + if (players.isEmpty()) { + builder.setTitle("Online Players"); + builder.setColor(Color.RED); + builder.setDescription("There are currently no players online"); + event.replyEmbeds(builder.build()).setEphemeral(true).queue(); + return; + } + + EmbedPaginator.Builder paginator = MessageUtil.defaultPaginator(event); + + /** + * Use Pagination to avoid message limits + */ + MessageUtil.listBatches(players, 10).forEach(p -> { + StringBuilder sb = new StringBuilder(); + count.getAndIncrement(); + builder.clear(); + builder.setTitle("Online Players - Page " + count.get() + "/" + (int)Math.ceil(((float)players.size() / 10))); + builder.setColor(Color.GREEN); + + p.forEach(account -> { + sb.append("`").append(account.getUsername()).append("`"); + + if (account.getDiscordUser() != null) { + sb.append(" - ").append(account.getDiscordUser().getAsMention()); + } + sb.append("\r\n"); + }); + + builder.setDescription(sb.toString()); + pages.add(builder.build()); + }); + + paginator.setItems(pages); + EmbedPaginator embedPaginator = paginator.build(); + + event.replyEmbeds(pages.get(0)).setEphemeral(false).queue(success -> + success.retrieveOriginal().queue(msg -> embedPaginator.paginate(msg, 1))); + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/general/ServerStatusSlashCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/general/ServerStatusSlashCommand.java new file mode 100644 index 0000000..6530772 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/general/ServerStatusSlashCommand.java @@ -0,0 +1,109 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.general; + +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.services.SDLinkPlatform; +import com.hypherionmc.sdlink.core.util.SystemUtils; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.HardwareAbstractionLayer; + +/** + * @author HypherionSA + * Informational command to give you a quick overview of the hardware/player + * status of your server + */ +public class ServerStatusSlashCommand extends SDLinkSlashCommand { + + public ServerStatusSlashCommand() { + super(true); + + this.name = "status"; + this.help = "View information about your server"; + this.guildOnly = true; + } + + @Override + protected void execute(SlashCommandEvent event) { + Button refreshBtn = Button.danger("sdrefreshbtn", "Refresh"); + event.replyEmbeds(runStatusCommand()).addActionRow(refreshBtn).queue(); + } + + public static MessageEmbed runStatusCommand() { + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hal = systemInfo.getHardware(); + CentralProcessor cpu = hal.getProcessor(); + + EmbedBuilder builder = new EmbedBuilder(); + builder.setTitle("Server Information / Status"); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("**__System Information__**\r\n\r\n"); + + stringBuilder + .append("**CPU:**\r\n```\r\n") + .append(cpu.toString()) + .append("```") + .append("\r\n"); + + try { + stringBuilder + .append("**Memory:**\r\n```\r\n") + .append(SystemUtils.byteToHuman(hal.getMemory().getAvailable())) + .append(" free of ") + .append(SystemUtils.byteToHuman(hal.getMemory().getTotal())) + .append("```\r\n"); + } catch (Exception e) { + } + + stringBuilder + .append("**OS:**\r\n```\r\n") + .append(systemInfo.getOperatingSystem().toString()) + .append(" (") + .append(systemInfo.getOperatingSystem().getBitness()) + .append(" bit)\r\n") + .append("Version: ") + .append(systemInfo.getOperatingSystem().getVersionInfo().getVersion()) + .append("```\r\n"); + + stringBuilder + .append("**System Uptime:**\r\n```\r\n") + .append(SystemUtils.secondsToTimestamp(systemInfo.getOperatingSystem().getSystemUptime())) + .append("```\r\n"); + + stringBuilder.append("**__Minecraft Information__**\r\n\r\n"); + + stringBuilder + .append("**Server Uptime:**\r\n```\r\n") + .append(SystemUtils.secondsToTimestamp(SDLinkPlatform.minecraftHelper.getServerUptime())) + .append("```\r\n"); + + stringBuilder + .append("**Server Version:**\r\n```\r\n") + .append(SDLinkPlatform.minecraftHelper.getServerVersion()) + .append("```\r\n"); + + stringBuilder + .append("**Players Online:**\r\n```\r\n") + .append(SDLinkPlatform.minecraftHelper.getPlayerCounts().getLeft()) + .append("/") + .append(SDLinkPlatform.minecraftHelper.getPlayerCounts().getRight()) + .append("```\r\n"); + + stringBuilder + .append("**Whitelisting:**\r\n```\r\n") + .append(!SDLinkPlatform.minecraftHelper.checkWhitelisting().isError() ? "Enabled" : "Disabled") + .append("```\r\n"); + + builder.setDescription(stringBuilder.toString()); + + return builder.build(); + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/ConfirmAccountLinkSlashCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/ConfirmAccountLinkSlashCommand.java new file mode 100644 index 0000000..14fb5a9 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/ConfirmAccountLinkSlashCommand.java @@ -0,0 +1,62 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.linking; + +import com.hypherionmc.sdlink.core.accounts.MinecraftAccount; +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.messaging.Result; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.Collections; +import java.util.List; + +import static com.hypherionmc.sdlink.core.managers.DatabaseManager.sdlinkDatabase; + +/** + * @author HypherionSA + * Command to complete Discord -> MC Account Linking + */ +public class ConfirmAccountLinkSlashCommand extends SDLinkSlashCommand { + + public ConfirmAccountLinkSlashCommand() { + super(false); + this.name = "confirmlink"; + this.help = "Confirm your Minecraft Account to complete account linking"; + + this.options = Collections.singletonList(new OptionData(OptionType.INTEGER, "code", "The verification code from the Minecraft Kick Message").setRequired(true)); + } + + @Override + protected void execute(SlashCommandEvent event) { + int mcCode = event.getOption("code") != null ? event.getOption("code").getAsInt() : 0; + + if (mcCode == 0) { + event.reply("You need to provide a verification code").setEphemeral(true).queue(); + return; + } + + List accounts = sdlinkDatabase.findAll(SDLinkAccount.class); + + if (accounts.isEmpty()) { + event.reply("Sorry, but this server does not contain any stored players in its database").setEphemeral(true).queue(); + return; + } + + for (SDLinkAccount account : accounts) { + if (account.getAccountLinkCode().equalsIgnoreCase(String.valueOf(mcCode))) { + MinecraftAccount minecraftAccount = MinecraftAccount.standard(account.getUsername()); + Result result = minecraftAccount.linkAccount(event.getMember(), event.getGuild()); + event.reply(result.getMessage()).setEphemeral(true).queue(); + return; + } + } + + event.reply("Sorry, we could not verify your Minecraft account. Please try again").setEphemeral(true).queue(); + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/LinkAccountCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/LinkAccountCommand.java new file mode 100644 index 0000000..3ac916f --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/LinkAccountCommand.java @@ -0,0 +1,78 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.linking; + +import com.hypherionmc.sdlink.core.accounts.MinecraftAccount; +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.util.SystemUtils; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import io.jsondb.InvalidJsonDbApiUsageException; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.Collections; + +import static com.hypherionmc.sdlink.core.managers.DatabaseManager.sdlinkDatabase; + +/** + * @author HypherionSA + * Command to start the Linking process of a Discord and MC Account + * This will generate the verification code the player needs to enter, to + * verify the account belongs to them + */ +public class LinkAccountCommand extends SDLinkSlashCommand { + + public LinkAccountCommand() { + super(false); + this.name = "linkaccount"; + this.help = "Start the process of linking your Discord and MC Accounts"; + + this.options = Collections.singletonList(new OptionData(OptionType.STRING, "mcname", "Your Minecraft Username").setRequired(true)); + } + + @Override + protected void execute(SlashCommandEvent event) { + String mcName = event.getOption("mcname") != null ? event.getOption("mcname").getAsString() : ""; + + if (mcName.isEmpty()) { + event.reply("You need to supply your Minecraft username").setEphemeral(true).queue(); + return; + } + + MinecraftAccount minecraftAccount = MinecraftAccount.standard(mcName); + String confirmCode = String.valueOf(SystemUtils.generateRandomJoinCode()); + SDLinkAccount account = minecraftAccount.getStoredAccount(); + + if (account == null) { + account = minecraftAccount.newDBEntry(); + account.setAccountLinkCode(confirmCode); + + try { + sdlinkDatabase.insert(account); + event.reply("Please join the Minecraft server and check the Kick Message for your account link code. Then, run the command /confirmlink codehere to finish linking your accounts").setEphemeral(true).queue(); + } catch (InvalidJsonDbApiUsageException e) { + e.printStackTrace(); + event.reply("Could not start account linking process. Please notify the server owner").setEphemeral(true).queue(); + } + } else { + if (account.getDiscordID() != null || !account.getDiscordID().isEmpty()) { + event.reply("Sorry, this Minecraft account is already linked to a discord account").setEphemeral(true).queue(); + return; + } + + account.setAccountLinkCode(confirmCode); + + try { + sdlinkDatabase.upsert(account); + event.reply("Please join the Minecraft server and check the Kick Message for your account link code. Then, run the command /confirmlink codehere to finish linking your accounts").setEphemeral(true).queue(); + } catch (InvalidJsonDbApiUsageException e) { + e.printStackTrace(); + event.reply("Could not start account linking process. Please notify the server owner").setEphemeral(true).queue(); + } + } + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/UnlinkAccountSlashCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/UnlinkAccountSlashCommand.java new file mode 100644 index 0000000..03bad73 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/UnlinkAccountSlashCommand.java @@ -0,0 +1,48 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.linking; + +import com.hypherionmc.sdlink.core.accounts.MinecraftAccount; +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.messaging.Result; +import com.jagrosh.jdautilities.command.SlashCommandEvent; + +import java.util.List; + +import static com.hypherionmc.sdlink.core.managers.DatabaseManager.sdlinkDatabase; + +/** + * @author HypherionSA + * Command to unlink a discord and minecraft account, that was previously linked + */ +public class UnlinkAccountSlashCommand extends SDLinkSlashCommand { + + public UnlinkAccountSlashCommand() { + super(false); + this.name = "unlinkaccount"; + this.help = "Unlink your previously linked Discord and Minecraft accounts"; + } + + @Override + protected void execute(SlashCommandEvent event) { + List accounts = sdlinkDatabase.findAll(SDLinkAccount.class); + + if (accounts.isEmpty()) { + event.reply("Sorry, but this server does not contain any stored players in its database").setEphemeral(true).queue(); + return; + } + + for (SDLinkAccount account : accounts) { + if (account.getDiscordID() != null && account.getDiscordID().equalsIgnoreCase(event.getMember().getId())) { + MinecraftAccount minecraftAccount = MinecraftAccount.standard(account.getUsername()); + Result result = minecraftAccount.unlinkAccount(); + event.reply(result.getMessage()).setEphemeral(true).queue(); + break; + } + } + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/ViewLinkedAccountsCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/ViewLinkedAccountsCommand.java new file mode 100644 index 0000000..ef4fee2 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/linking/ViewLinkedAccountsCommand.java @@ -0,0 +1,77 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.linking; + +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.util.MessageUtil; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import com.jagrosh.jdautilities.menu.EmbedPaginator; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.hypherionmc.sdlink.core.managers.DatabaseManager.sdlinkDatabase; + +/** + * @author HypherionSA + * Staff Command to view a list of Linked Minecraft and Discord accounts + */ +public class ViewLinkedAccountsCommand extends SDLinkSlashCommand { + + public ViewLinkedAccountsCommand() { + super(true); + + this.name = "linkedaccounts"; + this.help = "View a list of linked Discord and MC accounts"; + } + + @Override + protected void execute(SlashCommandEvent event) { + EmbedPaginator.Builder paginator = MessageUtil.defaultPaginator(event); + + List accounts = sdlinkDatabase.findAll(SDLinkAccount.class); + + EmbedBuilder builder = new EmbedBuilder(); + ArrayList pages = new ArrayList<>(); + AtomicInteger count = new AtomicInteger(); + + if (accounts.isEmpty()) { + event.reply("There are no linked accounts for this discord").setEphemeral(true).queue(); + return; + } + + MessageUtil.listBatches(accounts, 10).forEach(itm -> { + count.getAndIncrement(); + builder.clear(); + builder.setTitle("Linked Accounts - Page " + count + "/" + (int)Math.ceil(((float)accounts.size() / 10))); + builder.setColor(Color.GREEN); + StringBuilder sBuilder = new StringBuilder(); + + itm.forEach(v -> { + Member member = null; + + if (v.getDiscordID() != null && !v.getDiscordID().isEmpty()) { + member = event.getGuild().getMemberById(v.getDiscordID()); + } + + sBuilder.append(v.getUsername()).append(" -> ").append(member == null ? "Unlinked" : member.getAsMention()).append("\r\n"); + }); + builder.setDescription(sBuilder); + pages.add(builder.build()); + }); + + paginator.setItems(pages); + EmbedPaginator embedPaginator = paginator.build(); + + event.replyEmbeds(pages.get(0)).setEphemeral(false).queue(success -> success.retrieveOriginal().queue(msg -> embedPaginator.paginate(msg, 1))); + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/ConfirmWhitelistSlashCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/ConfirmWhitelistSlashCommand.java new file mode 100644 index 0000000..413f18d --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/ConfirmWhitelistSlashCommand.java @@ -0,0 +1,62 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.whitelist; + +import com.hypherionmc.sdlink.core.accounts.MinecraftAccount; +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.messaging.Result; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.Collections; +import java.util.List; + +import static com.hypherionmc.sdlink.core.managers.DatabaseManager.sdlinkDatabase; + +/** + * @author HypherionSA + * Command to confirm a Whitelist request + */ +public class ConfirmWhitelistSlashCommand extends SDLinkSlashCommand { + + public ConfirmWhitelistSlashCommand() { + super(false); + this.name = "whitelistconfirm"; + this.help = "Confirm your Minecraft Account to complete whitelisting"; + + this.options = Collections.singletonList(new OptionData(OptionType.INTEGER, "code", "The verification code from the Minecraft Kick Message").setRequired(true)); + } + + @Override + protected void execute(SlashCommandEvent event) { + int mcCode = event.getOption("code") != null ? event.getOption("code").getAsInt() : 0; + + if (mcCode == 0) { + event.reply("You need to provide a verification code").setEphemeral(true).queue(); + return; + } + + List accounts = sdlinkDatabase.findAll(SDLinkAccount.class); + + if (accounts.isEmpty()) { + event.reply("Sorry, but this server does not contain any stored players in its database").setEphemeral(true).queue(); + return; + } + + for (SDLinkAccount account : accounts) { + if (account.getWhitelistCode().equalsIgnoreCase(String.valueOf(mcCode))) { + MinecraftAccount minecraftAccount = MinecraftAccount.standard(account.getUsername()); + Result result = minecraftAccount.whitelistAccount(event.getMember(), event.getGuild()); + event.reply(result.getMessage()).setEphemeral(true).queue(); + return; + } + } + + event.reply("Sorry, we could not verify your Minecraft account. Please try again").setEphemeral(true).queue(); + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/UnWhitelistAccountSlashCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/UnWhitelistAccountSlashCommand.java new file mode 100644 index 0000000..6f80983 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/UnWhitelistAccountSlashCommand.java @@ -0,0 +1,53 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.whitelist; + +import com.hypherionmc.sdlink.core.accounts.MinecraftAccount; +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.messaging.Result; +import com.hypherionmc.sdlink.core.services.SDLinkPlatform; +import com.jagrosh.jdautilities.command.SlashCommandEvent; + +import java.util.List; + +import static com.hypherionmc.sdlink.core.managers.DatabaseManager.sdlinkDatabase; + +/** + * @author HypherionSA + * Remove a player from the whitelist, that was previously whitelisted through the bot + */ +public class UnWhitelistAccountSlashCommand extends SDLinkSlashCommand { + + public UnWhitelistAccountSlashCommand() { + super(false); + this.name = "unwhitelist"; + this.help = "Remove your previously whitelisted Minecraft account"; + } + + @Override + protected void execute(SlashCommandEvent event) { + List accounts = sdlinkDatabase.findAll(SDLinkAccount.class); + + if (accounts.isEmpty()) { + event.reply("Sorry, but this server does not contain any stored players in its database").setEphemeral(true).queue(); + return; + } + + for (SDLinkAccount account : accounts) { + if (account.getDiscordID().equalsIgnoreCase(event.getMember().getId())) { + MinecraftAccount minecraftAccount = MinecraftAccount.standard(account.getUsername()); + if (SDLinkPlatform.minecraftHelper.isPlayerWhitelisted(minecraftAccount).isError()) { + event.reply("Your account is not whitelisted in Minecraft. Cannot remove your account").setEphemeral(true).queue(); + } else { + Result result = minecraftAccount.unwhitelistAccount(); + event.reply(result.getMessage()).setEphemeral(true).queue(); + } + break; + } + } + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/ViewWhitelistedAccountsSlashCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/ViewWhitelistedAccountsSlashCommand.java new file mode 100644 index 0000000..cd06d68 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/ViewWhitelistedAccountsSlashCommand.java @@ -0,0 +1,78 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.whitelist; + +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.util.MessageUtil; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import com.jagrosh.jdautilities.menu.EmbedPaginator; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.MessageEmbed; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.hypherionmc.sdlink.core.managers.DatabaseManager.sdlinkDatabase; + +/** + * @author HypherionSA + * Staff command to view whitelisted players on their server + * This list only contains players whitelisted using the bot + */ +public class ViewWhitelistedAccountsSlashCommand extends SDLinkSlashCommand { + + public ViewWhitelistedAccountsSlashCommand() { + super(true); + + this.name = "whitelisted"; + this.help = "View a list of Whitelisted MC accounts"; + } + + @Override + protected void execute(SlashCommandEvent event) { + EmbedPaginator.Builder paginator = MessageUtil.defaultPaginator(event); + + List accounts = sdlinkDatabase.findAll(SDLinkAccount.class).stream().filter(SDLinkAccount::isWhitelisted).toList(); + + EmbedBuilder builder = new EmbedBuilder(); + ArrayList pages = new ArrayList<>(); + AtomicInteger count = new AtomicInteger(); + + if (accounts.isEmpty()) { + event.reply("There are no whitelisted accounts for this discord").setEphemeral(true).queue(); + return; + } + + MessageUtil.listBatches(accounts, 10).forEach(itm -> { + count.getAndIncrement(); + builder.clear(); + builder.setTitle("Whitelisted Accounts - Page " + count + "/" + (int)Math.ceil(((float)accounts.size() / 10))); + builder.setColor(Color.GREEN); + StringBuilder sBuilder = new StringBuilder(); + + itm.forEach(v -> { + Member member = null; + + if (v.getDiscordID() != null && !v.getDiscordID().isEmpty()) { + member = event.getGuild().getMemberById(v.getDiscordID()); + } + + sBuilder.append(v.getUsername()).append(" -> ").append(member == null ? "Unlinked" : member.getAsMention()).append("\r\n"); + }); + builder.setDescription(sBuilder); + pages.add(builder.build()); + }); + + paginator.setItems(pages); + EmbedPaginator embedPaginator = paginator.build(); + + event.replyEmbeds(pages.get(0)).setEphemeral(false).queue(success -> success.retrieveOriginal().queue(msg -> embedPaginator.paginate(msg, 1))); + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/WhitelistAccountCommand.java b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/WhitelistAccountCommand.java new file mode 100644 index 0000000..d1c930f --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/commands/slash/whitelist/WhitelistAccountCommand.java @@ -0,0 +1,81 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.commands.slash.whitelist; + +import com.hypherionmc.sdlink.core.accounts.MinecraftAccount; +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import com.hypherionmc.sdlink.core.discord.commands.slash.SDLinkSlashCommand; +import com.hypherionmc.sdlink.core.services.SDLinkPlatform; +import com.hypherionmc.sdlink.core.util.SystemUtils; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import io.jsondb.InvalidJsonDbApiUsageException; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; + +import java.util.Collections; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; +import static com.hypherionmc.sdlink.core.managers.DatabaseManager.sdlinkDatabase; + +/** + * @author HypherionSA + * Command to start the Whitelisting process + * This will generate the verification code the player needs to enter, to + * verify the account belongs to them + */ +public class WhitelistAccountCommand extends SDLinkSlashCommand { + + public WhitelistAccountCommand() { + super(sdLinkConfig.whitelistingAndLinking.whitelisting.staffOnlyWhitelist); + + this.name = "whitelist"; + this.help = "Start the process of Whitelisting your Minecraft Account"; + + this.options = Collections.singletonList(new OptionData(OptionType.STRING, "mcname", "Your Minecraft Username").setRequired(true)); + } + + @Override + protected void execute(SlashCommandEvent event) { + String mcName = event.getOption("mcname") != null ? event.getOption("mcname").getAsString() : ""; + + if (mcName.isEmpty()) { + event.reply("You need to supply your Minecraft username").setEphemeral(true).queue(); + return; + } + + MinecraftAccount minecraftAccount = MinecraftAccount.standard(mcName); + String confirmCode = String.valueOf(SystemUtils.generateRandomJoinCode()); + SDLinkAccount account = minecraftAccount.getStoredAccount(); + + if (account == null) { + account = minecraftAccount.newDBEntry(); + account.setWhitelistCode(confirmCode); + + try { + sdlinkDatabase.insert(account); + event.reply("Please join the Minecraft server and check the Kick Message for your account whitelist code. Then, run the command /whitelistconfirm codehere to finish whitelisting your account").setEphemeral(true).queue(); + } catch (InvalidJsonDbApiUsageException e) { + e.printStackTrace(); + event.reply("Could not start account whitelisting process. Please notify the server owner").setEphemeral(true).queue(); + } + } else { + if (!account.getDiscordID().isEmpty() || SDLinkPlatform.minecraftHelper.isPlayerWhitelisted(minecraftAccount).isError()) { + event.reply("Sorry, this Minecraft account is already whitelisted").setEphemeral(true).queue(); + return; + } + + account.setWhitelistCode(confirmCode); + + try { + sdlinkDatabase.upsert(account); + event.reply("Please join the Minecraft server and check the Kick Message for your account whitelist code. Then, run the command /whitelistconfirm codehere to finish whitelisting your account").setEphemeral(true).queue(); + } catch (InvalidJsonDbApiUsageException e) { + e.printStackTrace(); + event.reply("Could not start account whitelisting process. Please notify the server owner").setEphemeral(true).queue(); + } + } + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/events/DiscordEventHandler.java b/src/main/java/com/hypherionmc/sdlink/core/discord/events/DiscordEventHandler.java new file mode 100644 index 0000000..9d58ffa --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/events/DiscordEventHandler.java @@ -0,0 +1,70 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.events; + +import com.hypherionmc.sdlink.core.discord.BotController; +import com.hypherionmc.sdlink.core.discord.commands.slash.general.ServerStatusSlashCommand; +import com.hypherionmc.sdlink.core.discord.hooks.BotReadyHooks; +import com.hypherionmc.sdlink.core.discord.hooks.DiscordMessageHooks; +import com.hypherionmc.sdlink.core.managers.ChannelManager; +import com.hypherionmc.sdlink.core.managers.PermissionChecker; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.events.session.ReadyEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +/** + * @author HypherionSA + * Class to provide Hooks for Discord Events, such as message received, and login + * NOTE TO DEVELOPERS: Don't add ANY LOGIC IN HERE. Rather implement it in a seperate class, + * and use these hooks to trigger that code + */ +public class DiscordEventHandler extends ListenerAdapter { + + /** + * The bot received a message + */ + @Override + public void onMessageReceived(@NotNull MessageReceivedEvent event) { + if (event.isWebhookMessage()) + return; + + if (event.getAuthor() == event.getJDA().getSelfUser()) + return; + + if (!event.isFromGuild()) + return; + + DiscordMessageHooks.discordMessageEvent(event); + } + + /** + * The bot is connected to discord and ready to begin sending messages + */ + @Override + public void onReady(@NotNull ReadyEvent event) { + if (event.getJDA().getStatus() == JDA.Status.CONNECTED) { + BotController.INSTANCE.getLogger().info("Successfully connected to discord"); + + PermissionChecker.checkBotSetup(); + ChannelManager.loadChannels(); + BotReadyHooks.startActivityUpdates(event); + BotReadyHooks.startTopicUpdates(); + } + } + + /** + * A button was clicked. + */ + @Override + public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { + if (event.getComponentId().equals("sdrefreshbtn")) { + event.getMessage().editMessageEmbeds(ServerStatusSlashCommand.runStatusCommand()).queue(); + event.reply("Success!").setEphemeral(true).queue(); + } + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/hooks/BotReadyHooks.java b/src/main/java/com/hypherionmc/sdlink/core/discord/hooks/BotReadyHooks.java new file mode 100644 index 0000000..7316617 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/hooks/BotReadyHooks.java @@ -0,0 +1,82 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.hooks; + +import com.hypherionmc.sdlink.core.discord.BotController; +import com.hypherionmc.sdlink.core.managers.ChannelManager; +import com.hypherionmc.sdlink.core.messaging.MessageDestination; +import com.hypherionmc.sdlink.core.services.SDLinkPlatform; +import com.hypherionmc.sdlink.core.util.SystemUtils; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildMessageChannel; +import net.dv8tion.jda.api.events.session.ReadyEvent; + +import java.util.concurrent.TimeUnit; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Hooks to run when the bot is ready + */ +public class BotReadyHooks { + + /** + * Update the bot activity + * @param event The {@link ReadyEvent} + */ + public static void startActivityUpdates(ReadyEvent event) { + BotController.taskManager.scheduleAtFixedRate(() -> { + try { + if (event.getJDA().getStatus() == JDA.Status.CONNECTED) { + Activity act = Activity.of(sdLinkConfig.botConfig.botStatus.botStatusType, sdLinkConfig.botConfig.botStatus.botStatus + .replace("%players%", String.valueOf(SDLinkPlatform.minecraftHelper.getPlayerCounts().getLeft())) + .replace("%maxplayers%", String.valueOf(SDLinkPlatform.minecraftHelper.getPlayerCounts().getRight()))); + + if (sdLinkConfig.botConfig.botStatus.botStatusType == Activity.ActivityType.STREAMING) { + act = Activity.of(sdLinkConfig.botConfig.botStatus.botStatusType, sdLinkConfig.botConfig.botStatus.botStatus + .replace("%players%", String.valueOf(SDLinkPlatform.minecraftHelper.getPlayerCounts().getLeft())) + .replace("%maxplayers%", String.valueOf(SDLinkPlatform.minecraftHelper.getPlayerCounts().getRight())), + sdLinkConfig.botConfig.botStatus.botStatusStreamingURL); + } + + event.getJDA().getPresence().setActivity(act); + } + } catch (Exception e) { + if (sdLinkConfig.generalConfig.debugging) { + BotController.INSTANCE.getLogger().info(e.getMessage()); + } + } + }, sdLinkConfig.botConfig.statusUpdateInterval, sdLinkConfig.botConfig.statusUpdateInterval, TimeUnit.SECONDS); + } + + /** + * Update the Chat Channel topic, if enabled + */ + public static void startTopicUpdates() { + if (!sdLinkConfig.botConfig.channelTopic.doTopicUpdates) + return; + + BotController.taskManager.scheduleAtFixedRate(() -> { + try { + if (BotController.INSTANCE.isBotReady() && (sdLinkConfig.botConfig.channelTopic.channelTopic != null && !sdLinkConfig.botConfig.channelTopic.channelTopic.isEmpty())) { + StandardGuildMessageChannel channel = ChannelManager.getDestinationChannel(MessageDestination.CHAT); + if (channel != null) { + String topic = sdLinkConfig.botConfig.channelTopic.channelTopic + .replace("%players%", String.valueOf(SDLinkPlatform.minecraftHelper.getPlayerCounts().getLeft())) + .replace("%maxplayers%", String.valueOf(SDLinkPlatform.minecraftHelper.getPlayerCounts().getRight())) + .replace("%uptime%", SystemUtils.secondsToTimestamp(SDLinkPlatform.minecraftHelper.getServerUptime())); + channel.getManager().setTopic(topic).queue(); + } + } + } catch (Exception e) { + if (sdLinkConfig.generalConfig.debugging) { + BotController.INSTANCE.getLogger().info(e.getMessage()); + } + } + }, 6, 6, TimeUnit.MINUTES); + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/discord/hooks/DiscordMessageHooks.java b/src/main/java/com/hypherionmc/sdlink/core/discord/hooks/DiscordMessageHooks.java new file mode 100644 index 0000000..ad42d09 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/discord/hooks/DiscordMessageHooks.java @@ -0,0 +1,43 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.discord.hooks; + +import com.hypherionmc.sdlink.core.discord.BotController; +import com.hypherionmc.sdlink.core.managers.ChannelManager; +import com.hypherionmc.sdlink.core.messaging.MessageDestination; +import com.hypherionmc.sdlink.core.services.SDLinkPlatform; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Hook class to handle messages the bot receives + */ +public class DiscordMessageHooks { + + /** + * Chat messages to be sent back to discord + */ + public static void discordMessageEvent(MessageReceivedEvent event) { + try { + if (event.getChannel().getIdLong() != ChannelManager.getDestinationChannel(MessageDestination.CHAT).getIdLong()) + return; + + if (event.getAuthor().isBot() && sdLinkConfig.chatConfig.ignoreBots) + return; + + if (sdLinkConfig.generalConfig.debugging) { + BotController.INSTANCE.getLogger().info("Sending Message from {}: {}", event.getAuthor().getName(), event.getMessage().getContentStripped()); + } + SDLinkPlatform.minecraftHelper.discordMessageReceived(event.getMember().getEffectiveName(), event.getMessage().getContentRaw()); + } catch (Exception e) { + if (sdLinkConfig.generalConfig.debugging) { + e.printStackTrace(); + } + } + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/managers/ChannelManager.java b/src/main/java/com/hypherionmc/sdlink/core/managers/ChannelManager.java new file mode 100644 index 0000000..4a84eec --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/managers/ChannelManager.java @@ -0,0 +1,54 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.managers; + +import com.hypherionmc.sdlink.core.discord.BotController; +import com.hypherionmc.sdlink.core.messaging.MessageDestination; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildMessageChannel; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.HashMap; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Load and Cache configured channels for later use + */ +public class ChannelManager { + + private static StandardGuildMessageChannel consoleChannel; + + private static final HashMap> channelMap = new HashMap<>(); + + /** + * Load configured channel, while always defaulting back to ChatChannel for channels that aren't configured + */ + public static void loadChannels() { + channelMap.clear(); + + JDA jda = BotController.INSTANCE.getJDA(); + + StandardGuildMessageChannel chatChannel = jda.getChannelById(StandardGuildMessageChannel.class, sdLinkConfig.channelsAndWebhooks.channels.chatChannelID); + StandardGuildMessageChannel eventChannel = jda.getChannelById(StandardGuildMessageChannel.class, sdLinkConfig.channelsAndWebhooks.channels.eventsChannelID); + consoleChannel = jda.getChannelById(StandardGuildMessageChannel.class, sdLinkConfig.channelsAndWebhooks.channels.consoleChannelID); + + if (chatChannel != null) { + channelMap.put(MessageDestination.CHAT, Pair.of(chatChannel, false)); + } + + channelMap.put(MessageDestination.EVENT, eventChannel != null ? Pair.of(eventChannel, false) : Pair.of(chatChannel, false)); + channelMap.put(MessageDestination.CONSOLE, consoleChannel != null ? Pair.of(consoleChannel, true) : Pair.of(chatChannel, false)); + } + + public static StandardGuildMessageChannel getConsoleChannel() { + return consoleChannel; + } + + public static StandardGuildMessageChannel getDestinationChannel(MessageDestination destination) { + return channelMap.get(destination).getLeft(); + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/managers/DatabaseManager.java b/src/main/java/com/hypherionmc/sdlink/core/managers/DatabaseManager.java new file mode 100644 index 0000000..a15bf9c --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/managers/DatabaseManager.java @@ -0,0 +1,23 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.managers; + +import com.hypherionmc.sdlink.core.database.SDLinkAccount; +import io.jsondb.JsonDBTemplate; + +/** + * @author HypherionSA + * Helper class to initialize the JSON database + */ +public class DatabaseManager { + + public static final JsonDBTemplate sdlinkDatabase = new JsonDBTemplate("sdlinkstorage", "com.hypherionmc.sdlink.core.database"); + + public static void initialize() { + if (!sdlinkDatabase.collectionExists(SDLinkAccount.class)) { + sdlinkDatabase.createCollection(SDLinkAccount.class); + } + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/managers/PermissionChecker.java b/src/main/java/com/hypherionmc/sdlink/core/managers/PermissionChecker.java new file mode 100644 index 0000000..c89d0b9 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/managers/PermissionChecker.java @@ -0,0 +1,187 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.managers; + +import com.hypherionmc.sdlink.core.discord.BotController; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildMessageChannel; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Startup permission checker hook to check if the bot has all the required permissions to function + */ +public class PermissionChecker { + + // Invite URL for bot shown in server logs + private static final String DISCORD_INVITE = "https://discord.com/api/oauth2/authorize?client_id={bot_id}&permissions=3154463760&scope=bot%20applications.commands"; + + // Base Permissions required by the bot to operate + private static final List BOT_PERMS = new ArrayList<>() {{ + add(Permission.NICKNAME_CHANGE); + add(Permission.NICKNAME_MANAGE); + add(Permission.MANAGE_WEBHOOKS); + add(Permission.MESSAGE_SEND); + add(Permission.MESSAGE_EMBED_LINKS); + add(Permission.MESSAGE_HISTORY); + add(Permission.MESSAGE_EXT_EMOJI); + add(Permission.MANAGE_ROLES); + add(Permission.MESSAGE_MANAGE); + }}; + + // Basic channel permissions required by all channels + private static final List BASE_CHANNEL_PERMS = new ArrayList<>() {{ + add(Permission.VIEW_CHANNEL); + add(Permission.MESSAGE_SEND); + add(Permission.MESSAGE_EMBED_LINKS); + add(Permission.MANAGE_WEBHOOKS); + }}; + + /** + * Run the permission checker to see if the bot has all the required permissions + */ + public static void checkBotSetup() { + StringBuilder builder = new StringBuilder(); + builder.append("\r\n").append("******************* Simple Discord Link Errors *******************").append("\r\n"); + AtomicInteger errCount = new AtomicInteger(); + BotController controller = BotController.INSTANCE; + + if (!controller.isBotReady()) + return; + + controller.getLogger().info("Discord Invite Link for Bot: {}", DISCORD_INVITE.replace("{bot_id}", controller.getJDA().getSelfUser().getId())); + + if (controller.getJDA().getGuilds().isEmpty()) { + errCount.incrementAndGet(); + builder.append(errCount.get()) + .append(") ") + .append("Bot does not appear to be in any servers. You need to invite the bot to your discord server before chat relays will work. Use link ") + .append(DISCORD_INVITE.replace("{bot_id}", controller.getJDA().getSelfUser().getId())).append(" to invite the bot.") + .append("\r\n"); + } else { + if (controller.getJDA().getGuilds().size() > 1) { + errCount.incrementAndGet(); + builder.append(errCount.get()) + .append(") ") + .append("Bot appears to be in multiple discord servers. This mod is only designed to work with a single discord server") + .append("\r\n"); + } else { + Guild guild = controller.getJDA().getGuilds().get(0); + + if (guild != null) { + Member bot = guild.getMemberById(controller.getJDA().getSelfUser().getIdLong()); + EnumSet botPerms = bot.getPermissionsExplicit(); + + RoleManager.loadRequiredRoles(errCount, builder); + + if (!botPerms.contains(Permission.ADMINISTRATOR)) { + checkBotPerms(errCount, builder, botPerms); + + checkChannelPerms( + sdLinkConfig.channelsAndWebhooks.channels.chatChannelID, + "Chat Channel", + errCount, + builder, + bot, + true, + true + ); + + checkChannelPerms( + sdLinkConfig.channelsAndWebhooks.channels.eventsChannelID, + "Events Channel", + errCount, + builder, + bot, + false, + false + ); + + checkChannelPerms( + sdLinkConfig.channelsAndWebhooks.channels.consoleChannelID, + "Console Channel", + errCount, + builder, + bot, + false, + false + ); + } + } + } + } + + if (errCount.get() > 0) { + builder.append("\r\n").append("******************* Simple Discord Link Errors *******************").append("\r\n"); + controller.getLogger().info(builder.toString()); + } + } + + private static void checkBotPerms(AtomicInteger errCount, StringBuilder builder, EnumSet permissions) { + BOT_PERMS.forEach(perm -> { + if (!permissions.contains(perm)) { + errCount.incrementAndGet(); + builder.append(errCount.get()) + .append(") ") + .append("Missing Bot Permission: ") + .append(perm.getName()) + .append("\r\n"); + } + }); + } + + private static void checkChannelPerms(Long channelID, String channelName, AtomicInteger errCount, StringBuilder builder, Member bot, boolean channelRequired, boolean isChat) { + if (channelRequired && channelID == 0) { + errCount.incrementAndGet(); + builder.append(errCount.get()) + .append(") ") + .append(channelName) + .append(" ID is not set.... This value is required") + .append("\r\n"); + return; + } + + StandardGuildMessageChannel channel = BotController.INSTANCE.getJDA().getChannelById(StandardGuildMessageChannel.class, channelID); + + if (channel == null) { + errCount.incrementAndGet(); + builder.append(errCount.get()) + .append(") ") + .append(channelName) + .append(" ID does not point to a valid Discord Channel. Please double check this") + .append("\r\n"); + } else { + EnumSet chatPerms = bot.getPermissionsExplicit(channel); + + BASE_CHANNEL_PERMS.forEach(perm -> { + if (!chatPerms.contains(perm)) { + errCount.incrementAndGet(); + builder.append(errCount.get()) + .append(") ") + .append("Missing ") + .append(channelName) + .append(" Channel Permission: ") + .append(perm.getName()) + .append("\r\n"); + } + }); + + if (isChat) { + if (sdLinkConfig.botConfig.channelTopic.doTopicUpdates && !chatPerms.contains(Permission.MANAGE_CHANNEL)) { + errCount.incrementAndGet(); + builder.append(errCount.get()).append(") ").append("Missing Chat Channel Permission: Manage Channel. Topic updates will not work").append("\r\n"); + } + } + } + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/managers/RoleManager.java b/src/main/java/com/hypherionmc/sdlink/core/managers/RoleManager.java new file mode 100644 index 0000000..f5cd344 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/managers/RoleManager.java @@ -0,0 +1,106 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.managers; + +import com.hypherionmc.sdlink.core.discord.BotController; +import net.dv8tion.jda.api.entities.Role; + +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Load and Cache roles needed by the bot + */ +public class RoleManager { + + private static Role staffRole; + private static Role whitelistedRole; + private static Role linkedRole; + private static final HashMap commandRoles = new HashMap<>(); + + /** + * Check and load the roles required by the bot + * @param errCount + * @param builder + */ + public static void loadRequiredRoles(AtomicInteger errCount, StringBuilder builder) { + if (!sdLinkConfig.botConfig.staffRole.isEmpty()) { + staffRole = getRole(errCount, builder, "Staff", sdLinkConfig.botConfig.staffRole); + } + + if (!sdLinkConfig.whitelistingAndLinking.whitelisting.autoWhitelistRole.isEmpty()) { + whitelistedRole = getRole(errCount, builder, "Whitelist", sdLinkConfig.whitelistingAndLinking.whitelisting.autoWhitelistRole); + } + + if (!sdLinkConfig.whitelistingAndLinking.accountLinking.linkedRole.isEmpty()) { + linkedRole = getRole(errCount, builder, "Linked Account", sdLinkConfig.whitelistingAndLinking.accountLinking.linkedRole); + } + + if (sdLinkConfig.linkedCommands.enabled) { + commandRoles.clear(); + sdLinkConfig.linkedCommands.commands.forEach(cmd -> { + if (!cmd.discordRole.isEmpty()) { + Role role = getRole(errCount, builder, cmd.discordCommand + " usage", cmd.discordRole); + if (role != null) { + commandRoles.putIfAbsent(cmd.discordCommand, role); + } + } + }); + } + } + + /** + * Load a role from either a Name or ID + * @param errCount Counter holding the current error count + * @param builder String builder that is used to build the error messages + * @param roleIdentifier Log identifier for the role being loaded + * @param roleID The ID or Name of the role to load + * @return The role that matched or NULL + */ + private static Role getRole(AtomicInteger errCount, StringBuilder builder, String roleIdentifier, String roleID) { + Role role = BotController.INSTANCE.getJDA().getRoleById(roleID); + + if (role != null) { + return role; + } else { + List roles = BotController.INSTANCE.getJDA().getRolesByName(roleID, true); + if (!roles.isEmpty()) { + return roles.get(0); + } + } + + errCount.incrementAndGet(); + builder.append(errCount.get()) + .append(") ") + .append("Missing ") + .append(roleIdentifier) + .append(" Role. Role: ") + .append(roleID) + .append(" cannot be found in the server") + .append("\r\n"); + + return null; + } + + public static Role getLinkedRole() { + return linkedRole; + } + + public static Role getStaffRole() { + return staffRole; + } + + public static Role getWhitelistedRole() { + return whitelistedRole; + } + + public static HashMap getCommandRoles() { + return commandRoles; + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/managers/WebhookManager.java b/src/main/java/com/hypherionmc/sdlink/core/managers/WebhookManager.java new file mode 100644 index 0000000..f4faa59 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/managers/WebhookManager.java @@ -0,0 +1,86 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.managers; + +import club.minnced.discord.webhook.WebhookClient; +import com.hypherionmc.sdlink.core.discord.BotController; +import com.hypherionmc.sdlink.core.messaging.MessageDestination; +import com.hypherionmc.sdlink.core.messaging.SDLinkWebhookClient; +import com.hypherionmc.sdlink.core.util.EncryptionUtil; + +import java.util.HashMap; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Load and cache Webhook clients for later use + */ +public class WebhookManager { + + private static WebhookClient chatWebhookClient, eventWebhookClient, consoleWebhookClient; + private static final HashMap clientMap = new HashMap<>(); + + /** + * Load configured webhook clients + * Webhooks that are not configured, will use their Channel ID instead + */ + public static void init() { + clientMap.clear(); + + if (sdLinkConfig == null || !sdLinkConfig.channelsAndWebhooks.webhooks.enabled) + return; + + if (!sdLinkConfig.generalConfig.enabled) + return; + + if (!sdLinkConfig.channelsAndWebhooks.webhooks.chatWebhook.isEmpty()) { + chatWebhookClient = new SDLinkWebhookClient( + "Chat", + EncryptionUtil.INSTANCE.decrypt(sdLinkConfig.channelsAndWebhooks.webhooks.chatWebhook) + ).build(); + BotController.INSTANCE.getLogger().info("Using Webhook for Chat Messages"); + } + + if (!sdLinkConfig.channelsAndWebhooks.webhooks.eventsWebhook.isEmpty()) { + eventWebhookClient = new SDLinkWebhookClient( + "Events", + EncryptionUtil.INSTANCE.decrypt(sdLinkConfig.channelsAndWebhooks.webhooks.eventsWebhook) + ).build(); + BotController.INSTANCE.getLogger().info("Using Webhook for Event Messages"); + } + + if (!sdLinkConfig.channelsAndWebhooks.webhooks.consoleWebhook.isEmpty()) { + consoleWebhookClient = new SDLinkWebhookClient( + "Console", + EncryptionUtil.INSTANCE.decrypt(sdLinkConfig.channelsAndWebhooks.webhooks.consoleWebhook) + ).build(); + BotController.INSTANCE.getLogger().info("Using Webhook for Console Messages"); + } + + if (chatWebhookClient != null) { + clientMap.put(MessageDestination.CHAT, chatWebhookClient); + } + + clientMap.put(MessageDestination.EVENT, eventWebhookClient); + clientMap.put(MessageDestination.CONSOLE, consoleWebhookClient); + } + + public static WebhookClient getWebhookClient(MessageDestination destination) { + return clientMap.get(destination); + } + + public static void shutdown() { + if (chatWebhookClient != null) { + chatWebhookClient.close(); + } + if (eventWebhookClient != null) { + eventWebhookClient.close(); + } + if (consoleWebhookClient != null) { + consoleWebhookClient.close(); + } + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/messaging/MessageDestination.java b/src/main/java/com/hypherionmc/sdlink/core/messaging/MessageDestination.java new file mode 100644 index 0000000..b8c56ed --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/messaging/MessageDestination.java @@ -0,0 +1,27 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.messaging; + +/** + * @author HypherionSA + * Specifies to what channel a message should be delivered + */ +public enum MessageDestination { + CHAT, + EVENT, + CONSOLE; + + public boolean isChat() { + return this == CHAT; + } + + public boolean isEvent() { + return this == EVENT; + } + + public boolean isConsole() { + return this == CONSOLE; + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/messaging/MessageType.java b/src/main/java/com/hypherionmc/sdlink/core/messaging/MessageType.java new file mode 100644 index 0000000..df0efc7 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/messaging/MessageType.java @@ -0,0 +1,19 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.messaging; + +/** + * @author HypherionSA + * Used to specify the type of message being sent + */ +public enum MessageType { + CHAT, + START_STOP, + JOIN_LEAVE, + ADVANCEMENT, + DEATH, + COMMAND, + CONSOLE +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/messaging/Result.java b/src/main/java/com/hypherionmc/sdlink/core/messaging/Result.java new file mode 100644 index 0000000..0fcbda9 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/messaging/Result.java @@ -0,0 +1,42 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.messaging; + +/** + * @author HypherionSA + * Helper Class to return the result of interactions between Discord and Minecraft + */ +public class Result { + + enum Type { + ERROR, + SUCCESS + } + + private final Type type; + private final String message; + + private Result(Type type, String message) { + this.type = type; + this.message = message; + } + + public static Result success(String message) { + return new Result(Type.SUCCESS, message); + } + + public static Result error(String message) { + return new Result(Type.ERROR, message); + } + + public boolean isError() { + return this.type == Type.ERROR; + } + + public String getMessage() { + return message; + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/messaging/SDLinkWebhookClient.java b/src/main/java/com/hypherionmc/sdlink/core/messaging/SDLinkWebhookClient.java new file mode 100644 index 0000000..8401fe9 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/messaging/SDLinkWebhookClient.java @@ -0,0 +1,27 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.messaging; + +import club.minnced.discord.webhook.WebhookClientBuilder; + +/** + * @author HypherionSA + * Wrapped {@link WebhookClientBuilder} for our webhooks + */ +public class SDLinkWebhookClient extends WebhookClientBuilder { + + public SDLinkWebhookClient(String name, String url) { + super(url); + + this.setThreadFactory((job) -> { + Thread thread = new Thread(job); + thread.setName(name + " Webhook Thread"); + thread.setDaemon(true); + return thread; + }); + this.setWait(false); + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/messaging/discord/DiscordMessage.java b/src/main/java/com/hypherionmc/sdlink/core/messaging/discord/DiscordMessage.java new file mode 100644 index 0000000..3f86d0e --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/messaging/discord/DiscordMessage.java @@ -0,0 +1,209 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.messaging.discord; + +import club.minnced.discord.webhook.WebhookClient; +import club.minnced.discord.webhook.send.WebhookEmbed; +import club.minnced.discord.webhook.send.WebhookEmbedBuilder; +import club.minnced.discord.webhook.send.WebhookMessageBuilder; +import com.hypherionmc.sdlink.core.accounts.DiscordAuthor; +import com.hypherionmc.sdlink.core.config.impl.MessageChannelConfig; +import com.hypherionmc.sdlink.core.discord.BotController; +import com.hypherionmc.sdlink.core.managers.ChannelManager; +import com.hypherionmc.sdlink.core.managers.WebhookManager; +import com.hypherionmc.sdlink.core.messaging.MessageType; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildMessageChannel; +import org.apache.commons.lang3.tuple.Triple; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Represents a message sent from Minecraft to Discord + * This ensures the message is properly formatted and configured + */ +public final class DiscordMessage { + + private final MessageType messageType; + private final DiscordAuthor author; + private final String message; + private final Runnable afterSend; + + /** + * Private instance. Use {@link DiscordMessageBuilder} to create an instance + */ + DiscordMessage(DiscordMessageBuilder builder) { + this.messageType = builder.getMessageType(); + this.author = builder.getAuthor(); + this.message = builder.getMessage(); + this.afterSend = builder.getAfterSend(); + } + + /** + * Try to send the message to discord + */ + public void sendMessage() { + if (!BotController.INSTANCE.isBotReady()) + return; + + try { + if (messageType == MessageType.CONSOLE) { + sendConsoleMessage(); + } else { + sendNormalMessage(); + } + } catch (Exception e) { + if (sdLinkConfig.generalConfig.debugging) { + BotController.INSTANCE.getLogger().error("Failed to send Discord Message", e); + } + } + } + + /** + * Send a Non Console relay message to discord + */ + private void sendNormalMessage() { + Triple channel = resolveDestination(); + + // Check if a webhook is configured, and use that instead + if (channel.getMiddle() != null && sdLinkConfig.channelsAndWebhooks.webhooks.enabled) { + WebhookMessageBuilder builder = new WebhookMessageBuilder(); + builder.setUsername(this.author.getUsername()); + if (!this.author.getAvatar().isEmpty()) { + builder.setAvatarUrl(this.author.getAvatar()); + } + + // Message must be an Embed + if (channel.getRight()) { + EmbedBuilder eb = buildEmbed(false); + WebhookEmbed web = WebhookEmbedBuilder.fromJDA(eb.build()).build(); + builder.addEmbeds(web); + } else { + builder.setContent(message); + } + + channel.getMiddle().send(builder.build()); + } else { + // Use the configured channel instead + if (channel.getRight()) { + EmbedBuilder eb = buildEmbed(true); + channel.getLeft().sendMessageEmbeds(eb.build()).queue(); + } else { + channel.getLeft().sendMessage( + this.messageType == MessageType.CHAT ? + sdLinkConfig.messageFormatting.chat.replace("%player%", author.getUsername()).replace("%message%", message) + : message) + .queue(success -> { + if (afterSend != null) { + afterSend.run(); + } + }); + } + } + } + + /** + * Only used for console relay messages + */ + private void sendConsoleMessage() { + try { + if (!BotController.INSTANCE.isBotReady() || !sdLinkConfig.chatConfig.sendConsoleMessages) + return; + + StandardGuildMessageChannel channel = ChannelManager.getConsoleChannel(); + if (channel != null) { + channel.sendMessage(this.message).queue(); + } + } catch (Exception e) { + if (sdLinkConfig.generalConfig.debugging) { + BotController.INSTANCE.getLogger().error("Failed to send console message", e); + } + } + + if (afterSend != null) { + afterSend.run(); + } + } + + /** + * Build an embed with the supplied information + * @param withAuthor Should the author be appended to the embed. Not used for Webhooks + */ + private EmbedBuilder buildEmbed(boolean withAuthor) { + EmbedBuilder builder = new EmbedBuilder(); + + if (withAuthor) { + builder.setAuthor( + this.author.getUsername(), + null, + this.author.getAvatar().isEmpty() ? null : this.author.getAvatar() + ); + } + + builder.setDescription(message); + return builder; + } + + /** + * Figure out where the message must be delivered to, based on the config values + */ + private Triple resolveDestination() { + switch (messageType) { + case CHAT -> { + MessageChannelConfig.DestinationObject chat = sdLinkConfig.messageDestinations.chat; + return Triple.of( + ChannelManager.getDestinationChannel(chat.channel), + WebhookManager.getWebhookClient(chat.channel), + chat.useEmbed + ); + } + case START_STOP -> { + MessageChannelConfig.DestinationObject startStop = sdLinkConfig.messageDestinations.startStop; + return Triple.of( + ChannelManager.getDestinationChannel(startStop.channel), + WebhookManager.getWebhookClient(startStop.channel), + startStop.useEmbed + ); + } + case JOIN_LEAVE -> { + MessageChannelConfig.DestinationObject joinLeave = sdLinkConfig.messageDestinations.joinLeave; + return Triple.of( + ChannelManager.getDestinationChannel(joinLeave.channel), + WebhookManager.getWebhookClient(joinLeave.channel), + joinLeave.useEmbed + ); + } + case ADVANCEMENT -> { + MessageChannelConfig.DestinationObject advancement = sdLinkConfig.messageDestinations.advancements; + return Triple.of( + ChannelManager.getDestinationChannel(advancement.channel), + WebhookManager.getWebhookClient(advancement.channel), + advancement.useEmbed + ); + } + case DEATH -> { + MessageChannelConfig.DestinationObject death = sdLinkConfig.messageDestinations.death; + return Triple.of( + ChannelManager.getDestinationChannel(death.channel), + WebhookManager.getWebhookClient(death.channel), + death.useEmbed + ); + } + case COMMAND -> { + MessageChannelConfig.DestinationObject command = sdLinkConfig.messageDestinations.commands; + return Triple.of( + ChannelManager.getDestinationChannel(command.channel), + WebhookManager.getWebhookClient(command.channel), + command.useEmbed + ); + } + } + + // This code should never be reached, but it's added here as a fail-safe + MessageChannelConfig.DestinationObject chat = sdLinkConfig.messageDestinations.chat; + return Triple.of(ChannelManager.getDestinationChannel(chat.channel), WebhookManager.getWebhookClient(chat.channel), chat.useEmbed); + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/messaging/discord/DiscordMessageBuilder.java b/src/main/java/com/hypherionmc/sdlink/core/messaging/discord/DiscordMessageBuilder.java new file mode 100644 index 0000000..40316eb --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/messaging/discord/DiscordMessageBuilder.java @@ -0,0 +1,88 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.messaging.discord; + +import com.hypherionmc.sdlink.core.accounts.DiscordAuthor; +import com.hypherionmc.sdlink.core.messaging.MessageType; + +/** + * @author HypherionSA + * Used to construct a {@link DiscordMessage} to be sent back to discord + */ +public final class DiscordMessageBuilder { + + private final MessageType messageType; + private DiscordAuthor author; + private String message; + private Runnable afterSend; + + /** + * Construct a discord message + * @param messageType The type of message being sent + */ + public DiscordMessageBuilder(MessageType messageType) { + this.messageType = messageType; + } + + /** + * Add an Author to the message + */ + public DiscordMessageBuilder author(DiscordAuthor author) { + this.author = author; + + if (author.getUsername().equalsIgnoreCase("server")) { + this.author = DiscordAuthor.SERVER; + } + + return this; + } + + /** + * The Actual message that will be sent + */ + public DiscordMessageBuilder message(String message) { + message = message.replace("<@", ""); + message = message.replace("@everyone", ""); + message = message.replace("@here", ""); + this.message = message; + return this; + } + + public DiscordMessageBuilder afterSend(Runnable afterSend) { + this.afterSend = afterSend; + return this; + } + + /** + * Build a Discord Message ready to be sent + */ + public DiscordMessage build() { + if (this.author == null) { + this.author = DiscordAuthor.SERVER; + } + + if (this.message == null) { + this.message = ""; + } + + return new DiscordMessage(this); + } + + public String getMessage() { + return message; + } + + public MessageType getMessageType() { + return messageType; + } + + public DiscordAuthor getAuthor() { + return author; + } + + public Runnable getAfterSend() { + return afterSend; + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/services/SDLinkPlatform.java b/src/main/java/com/hypherionmc/sdlink/core/services/SDLinkPlatform.java new file mode 100644 index 0000000..6b05844 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/services/SDLinkPlatform.java @@ -0,0 +1,28 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.services; + +import com.hypherionmc.sdlink.core.discord.BotController; +import com.hypherionmc.sdlink.core.services.helpers.IMinecraftHelper; + +import java.util.ServiceLoader; + +/** + * @author HypherionSA + * Service loader for library services + */ +public class SDLinkPlatform { + + public static IMinecraftHelper minecraftHelper = load(IMinecraftHelper.class); + + public static T load(Class clazz) { + final T loadedService = ServiceLoader.load(clazz) + .findFirst() + .orElseThrow(() -> new NullPointerException("Failed to load service for " + clazz.getName())); + BotController.INSTANCE.getLogger().debug("Loaded {} for service {}", loadedService, clazz); + return loadedService; + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/services/helpers/IMinecraftHelper.java b/src/main/java/com/hypherionmc/sdlink/core/services/helpers/IMinecraftHelper.java new file mode 100644 index 0000000..32e3673 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/services/helpers/IMinecraftHelper.java @@ -0,0 +1,32 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.services.helpers; + +import com.hypherionmc.sdlink.core.accounts.MinecraftAccount; +import com.hypherionmc.sdlink.core.messaging.Result; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.List; + +/** + * @author HypherionSA + * Service to bridge communication between the Library and Minecraft + */ +public interface IMinecraftHelper { + + void discordMessageReceived(String username, String message); + Result checkWhitelisting(); + Result isPlayerWhitelisted(MinecraftAccount account); + Result whitelistPlayer(MinecraftAccount account); + Result unWhitelistPlayer(MinecraftAccount account); + List getWhitelistedPlayers(); + Pair getPlayerCounts(); + List getOnlinePlayers(); + long getServerUptime(); + String getServerVersion(); + Result executeMinecraftCommand(String command, String args); + boolean isOnlineMode(); + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/util/EncryptionUtil.java b/src/main/java/com/hypherionmc/sdlink/core/util/EncryptionUtil.java new file mode 100644 index 0000000..468b796 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/util/EncryptionUtil.java @@ -0,0 +1,126 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.util; + +import com.hypherionmc.sdlink.core.discord.BotController; +import org.apache.commons.io.FileUtils; +import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; +import org.jasypt.exceptions.EncryptionOperationNotPossibleException; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.Random; + +/** + * @author HypherionSA + * Util Class to handle Encryption/Decryption of Bot-Tokens and Webhook URLS + * Since people DON'T READ THE COMMENTS and leave these in-tact, + * they are now encrypted by default + */ +public final class EncryptionUtil { + + public static EncryptionUtil INSTANCE = getInstance(); + private final boolean canRun; + + // Instance of the Encryptor Used + private final StandardPBEStringEncryptor encryptor; + + private static EncryptionUtil getInstance() { + if (INSTANCE == null) { + INSTANCE = new EncryptionUtil(); + } + return INSTANCE; + } + + private EncryptionUtil() { + String encCode = ""; + + File storageDir = new File("sdlinkstorage"); + if (storageDir.exists()) + storageDir.mkdirs(); + + // Try to read a saved encryption key, or try to save a new one + try { + File encKey = new File(storageDir.getAbsolutePath() + File.separator + "sdlink.enc"); + if (!encKey.exists()) { + FileUtils.writeStringToFile(encKey, getSaltString(), StandardCharsets.UTF_8); + } else { + encCode = FileUtils.readFileToString(encKey, StandardCharsets.UTF_8); + } + } catch (Exception e) { + BotController.INSTANCE.getLogger().error("Failed to initialize Encryption", e); + } + + encryptor = new StandardPBEStringEncryptor(); + encryptor.setPassword(encCode); + + canRun = !encCode.isEmpty(); + } + + /** + * Will Encrypt the string passed into it, if it's not already encrypted + * @param input The string to be encrypted + * @return The encrypted string + */ + public String encrypt(String input) { + if (!canRun) + return input; + + if (isEncrypted(input)) { + return input; + } + + input = "enc:" + input; + return encryptor.encrypt(input); + } + + /** + * Decrypts an encrypted string + * @param input The encrypted String + * @return The Plain Text String + */ + public String decrypt(String input) { + if (!canRun) + return input; + + input = internalDecrypt(input); + + if (input.startsWith("enc:")) { + input = input.replaceFirst("enc:", ""); + } + return input; + } + + // Used internally + private String internalDecrypt(String input) { + if (!canRun) + return input; + return encryptor.decrypt(input); + } + + // Test if String is encrypted + private boolean isEncrypted(String input) { + try { + String temp = internalDecrypt(input); + return temp.startsWith("enc:"); + } catch (EncryptionOperationNotPossibleException ignored) { + // String is likely not encrypted + } + return false; + } + + // Generate Random codes for encryption/decryption + private String getSaltString() { + String SALTCHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + StringBuilder salt = new StringBuilder(); + Random rnd = new Random(); + while (salt.length() < 18) { + int index = (int) (rnd.nextFloat() * SALTCHARS.length()); + salt.append(SALTCHARS.charAt(index)); + } + return salt.toString(); + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/util/LogReader.java b/src/main/java/com/hypherionmc/sdlink/core/util/LogReader.java new file mode 100644 index 0000000..43b9cb7 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/util/LogReader.java @@ -0,0 +1,126 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.util; + +import com.hypherionmc.sdlink.core.accounts.DiscordAuthor; +import com.hypherionmc.sdlink.core.discord.BotController; +import com.hypherionmc.sdlink.core.messaging.MessageType; +import com.hypherionmc.sdlink.core.messaging.discord.DiscordMessage; +import com.hypherionmc.sdlink.core.messaging.discord.DiscordMessageBuilder; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +import static com.hypherionmc.sdlink.core.config.ConfigController.sdLinkConfig; + +/** + * @author HypherionSA + * Log Appender to allow messages to be relayed from the Game Console to Discord + */ +@Plugin(name = "SDLinkLogging", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class LogReader extends AbstractAppender { + + public static String logs = ""; + private long time; + private Thread messageScheduler; + private static boolean isDevEnv = false; + + protected LogReader(String name, Filter filter) { + super(name, filter, null, true, new Property[0]); + } + + @PluginFactory + public static LogReader createAppender( + @PluginAttribute("name") String name, + @PluginElement("Filter") Filter filter) { + return new LogReader(name, filter); + } + + public static void init(boolean isDev) { + isDevEnv = isDev; + LogReader da = LogReader.createAppender("SDLinkLogging", null); + ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addAppender(da); + da.start(); + } + + @Override + public void append(LogEvent event) { + if (BotController.INSTANCE.isBotReady()) { + if (event.getLevel().intLevel() < Level.DEBUG.intLevel()) { + logs += formatMessage(event) + "\n"; + scheduleMessage(); + } + } + } + + private String formatMessage(LogEvent event) { + String devString = "**[" + formatTime(event.getTimeMillis()) + "]** " + + "**[" + event.getThreadName() + "/" + event.getLevel().name() + "]** " + + "**(" + event.getLoggerName().substring(event.getLoggerName().lastIndexOf(".") + 1) + ")** *" + + event.getMessage().getFormattedMessage() + "*"; + + String prodString = "**[" + formatTime(event.getTimeMillis()) + "]** " + + "**[" + event.getThreadName() + "/" + event.getLevel().name() + "]** *" + + event.getMessage().getFormattedMessage() + "*"; + + return isDevEnv ? devString : prodString; + } + + private String formatTime(long millis) { + DateFormat obj = new SimpleDateFormat("HH:mm:ss"); + Date res = new Date(millis); + return obj.format(res); + } + + private void scheduleMessage() { + time = System.currentTimeMillis(); + if (messageScheduler == null || !messageScheduler.isAlive()) { + messageScheduler = new Thread(() -> { + while (true) { + if (!BotController.INSTANCE.isBotReady()) + return; + if (System.currentTimeMillis() - time > 250) { + if (logs.length() > 2000) { + logs = logs.substring(0, 1999); + } + + DiscordMessage discordMessage = new DiscordMessageBuilder(MessageType.CONSOLE) + .message(logs) + .author(DiscordAuthor.SERVER) + .build(); + + if (sdLinkConfig.chatConfig.sendConsoleMessages) { + discordMessage.sendMessage(); + } + + logs = ""; + break; + } + try { + Thread.sleep(30); + } catch (InterruptedException e) { + if (sdLinkConfig.generalConfig.debugging) { + BotController.INSTANCE.getLogger().error("Failed to send console message: {}", e.getMessage()); + } + } + } + }); + messageScheduler.start(); + } + } +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/util/MessageUtil.java b/src/main/java/com/hypherionmc/sdlink/core/util/MessageUtil.java new file mode 100644 index 0000000..51607e6 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/util/MessageUtil.java @@ -0,0 +1,78 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.util; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.hypherionmc.sdlink.core.discord.BotController; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import com.jagrosh.jdautilities.menu.EmbedPaginator; +import net.dv8tion.jda.api.exceptions.PermissionException; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * @author HypherionSA + * Util classes to help manage certain discord message actions + */ +public class MessageUtil { + + /** + * Create an Embed Paginator for use with Slash Commands + * @param event The event of the executed command + */ + public static EmbedPaginator.Builder defaultPaginator(SlashCommandEvent event) { + return new EmbedPaginator.Builder() + .setTimeout(1, TimeUnit.MINUTES) + .setEventWaiter(BotController.INSTANCE.getEventWaiter()) + .waitOnSinglePage(true) + .setFinalAction(m -> { + try { + m.clearReactions().queue(); + m.delete().queue(); + } catch(PermissionException ex) { + ex.printStackTrace(); + event.reply(ex.getMessage()).setEphemeral(true).queue(); + } + }) + .setText((BiFunction) null); + } + + /** + * Split a large list of items into smaller sublists. This is to help with Discord limits on pagination + * @param source The list of objects to split + * @param length How many entries are allowed per sub-list + */ + public static Stream> listBatches(List source, int length) { + if (length <= 0) + throw new IllegalArgumentException("length = " + length); + int size = source.size(); + if (size <= 0) + return Stream.empty(); + int fullChunks = (size - 1) / length; + return IntStream.range(0, fullChunks + 1).mapToObj( + n -> source.subList(n * length, n == fullChunks ? size : (n + 1) * length)); + } + + /** + * Same as {@link #listBatches(List, int)}, but for HashMaps + */ + public static List> splitMap(Map map, int size) { + List>> list = Lists.newArrayList(Iterables.partition(map.entrySet(), size)); + + return list.stream() + .map(entries -> + entries.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) + ) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/util/SystemUtils.java b/src/main/java/com/hypherionmc/sdlink/core/util/SystemUtils.java new file mode 100644 index 0000000..867e421 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/util/SystemUtils.java @@ -0,0 +1,101 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.util; + +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +public class SystemUtils { + + /** + * Convert Bytes into a human-readable format, like 1GB + * From https://stackoverflow.com/a/3758880 + * @param bytes The Size in Bytes + * @return The size formatted in KB, MB, GB, TB, PB etc + */ + public static String byteToHuman(long bytes) { + long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes); + if (absB < 1024) { + return bytes + " B"; + } + long value = absB; + CharacterIterator ci = new StringCharacterIterator("KMGTPE"); + for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) { + value >>= 10; + ci.next(); + } + value *= Long.signum(bytes); + return String.format("%.1f %ciB", value / 1024.0, ci.current()); + } + + // Time Conversion + public static final List times = Arrays.asList( + TimeUnit.DAYS.toMillis(365), + TimeUnit.DAYS.toMillis(30), + TimeUnit.DAYS.toMillis(1), + TimeUnit.HOURS.toMillis(1), + TimeUnit.MINUTES.toMillis(1), + TimeUnit.SECONDS.toMillis(1)); + + public static final List timesString = Arrays.asList("year", "month", "day", "hour", "minute", "second"); + + /** + * Unix Timestamp to Duration + * @param duration Unix Timestamp + * @return Formatted Duration + */ + public static String toDuration(long duration) { + StringBuffer res = new StringBuffer(); + for (int i = 0; i < times.size(); i++) { + Long current = times.get(i); + long temp = duration / current; + if (temp > 0) { + res.append(temp).append(" ").append(timesString.get(i)).append(temp != 1 ? "s" : ""); + break; + } + } + if ("".equals(res.toString())) + return "0 seconds ago"; + else + return res.toString(); + } + + /** + * Convert Seconds into a Timestamp + * @param sec Input in seconds + */ + public static String secondsToTimestamp(long sec) { + long seconds = sec % 60; + long minutes = (sec / 60) % 60; + long hours = (sec / 3600) % 24; + long days = sec / (3600 * 24); + + String timeString = String.format("%02d hour(s), %02d minute(s), %02d second(s)", hours, minutes, seconds); + + if (days > 0) { + timeString = String.format("%d day(s), %s", days, timeString); + } + + return timeString; + } + + /** + * Generate random verification code for Whitelisting and Account Linking + */ + public static int generateRandomJoinCode() { + return new Random().ints(1000, 9999).findFirst().getAsInt(); + } + + /*public static boolean hasPermission(BotController controller, Member member) { + if (controller.getAdminRole() != null) { + return member.getRoles().stream().anyMatch(r -> r.getIdLong() == controller.getAdminRole().getIdLong()); + } + return member.hasPermission(Permission.ADMINISTRATOR) || member.hasPermission(Permission.KICK_MEMBERS); + }*/ +} diff --git a/src/main/java/com/hypherionmc/sdlink/core/util/ThreadedEventManager.java b/src/main/java/com/hypherionmc/sdlink/core/util/ThreadedEventManager.java new file mode 100644 index 0000000..7c5caf1 --- /dev/null +++ b/src/main/java/com/hypherionmc/sdlink/core/util/ThreadedEventManager.java @@ -0,0 +1,22 @@ +/* + * This file is part of sdlink-core, licensed under the MIT License (MIT). + * Copyright HypherionSA and Contributors + */ +package com.hypherionmc.sdlink.core.util; + +import com.hypherionmc.sdlink.core.discord.BotController; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.hooks.InterfacedEventManager; +import org.jetbrains.annotations.NotNull; + +/** + * @author HypherionSA + * Run discord events in seperate threads + */ +public class ThreadedEventManager extends InterfacedEventManager { + + @Override + public void handle(@NotNull GenericEvent event) { + BotController.taskManager.submit(() -> super.handle(event)); + } +}