diff --git a/.gitignore b/.gitignore index e165b2c..f1cd263 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ gradle-app.setting .project # JDT-specific (Eclipse Java Development Tools) .classpath + +.idea/ +/tmp/ diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 8306744..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3e30b74..830f1fc 100644 --- a/build.gradle +++ b/build.gradle @@ -30,13 +30,13 @@ dependencies { implementation group: 'com.vdurmont', name: 'emoji-java', version: '5.1.1' implementation group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1' implementation 'io.vavr:vavr:0.10.4' - implementation 'com.google.code.gson:gson:2.9.0' + implementation 'com.google.code.gson:gson:2.14.0' implementation 'com.github.Alan19:Velen:3e73a4b331' - implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.2' - implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.2' implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.36.0.3' implementation 'org.jgrapht:jgrapht-jdk1.5:0.7.3' implementation 'com.io7m.jgrapht:jgrapht-guava:1.2.1-MODULARIZED' + implementation 'org.slf4j:slf4j-simple:2.0.18' + implementation 'org.apache.logging.log4j:log4j-to-slf4j:2.26.1' } distZip { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 28861d2..afba109 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..b371555 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.6-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0..65dcd68 100644 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# 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. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/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 -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # 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" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + 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 @@ -106,80 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $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 \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd3..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/resources/languages.json b/resources/languages.json index 1fd0747..6606d66 100644 --- a/resources/languages.json +++ b/resources/languages.json @@ -453,7 +453,7 @@ "to": "Cosma" }, { - "from": "Coimperial", + "from": "Corimperial", "to": "Cosma" }, { @@ -466,7 +466,7 @@ }, { "from": "Imperial English", - "to": "Coimperial" + "to": "Corimperial" }, { "from": "Starword", diff --git a/resources/settings.json b/resources/settings.json index 32d6a14..a8e7448 100644 --- a/resources/settings.json +++ b/resources/settings.json @@ -10,5 +10,9 @@ "token": "", "storytellerRoles": [] }, + "remoteDataSettings": { + "url": "http://localhost:8080", + "token": "" + }, "players": [] } \ No newline at end of file diff --git a/src/main/java/calculation/outputs/BigQueryOutput.java b/src/main/java/calculation/outputs/BigQueryOutput.java index c7c8889..9967ad6 100644 --- a/src/main/java/calculation/outputs/BigQueryOutput.java +++ b/src/main/java/calculation/outputs/BigQueryOutput.java @@ -4,8 +4,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.vavr.control.Try; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.BufferedWriter; @@ -19,7 +19,7 @@ * Output class for output a json new line delimited form for Google's Big Query. Dice end up being an array object in the Roll Object */ public class BigQueryOutput implements IOutput { - private final static Logger LOGGER = LogManager.getLogger(BigQueryOutput.class); + private final static Logger LOGGER = LoggerFactory.getLogger(BigQueryOutput.class); private final static Gson GSON = new GsonBuilder() .create(); diff --git a/src/main/java/calculation/outputs/SQLOutput.java b/src/main/java/calculation/outputs/SQLOutput.java index 53a0474..ef08626 100644 --- a/src/main/java/calculation/outputs/SQLOutput.java +++ b/src/main/java/calculation/outputs/SQLOutput.java @@ -5,8 +5,8 @@ import calculation.models.RollInfo; import io.vavr.control.Try; import org.apache.commons.lang3.tuple.Pair; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import rolling.Roller; import javax.annotation.Nullable; @@ -24,7 +24,7 @@ * Output that creates a SQLite Database with two Tables: rolls and dice with rolls being the parent table */ public class SQLOutput implements IOutput, AutoCloseable { - private static final Logger LOGGER = LogManager.getLogger(SQLOutput.class); + private static final Logger LOGGER = LoggerFactory.getLogger(SQLOutput.class); private final Connection connection; private final String fileLocation; diff --git a/src/main/java/configs/RemoteDataSettings.java b/src/main/java/configs/RemoteDataSettings.java new file mode 100644 index 0000000..196d9f7 --- /dev/null +++ b/src/main/java/configs/RemoteDataSettings.java @@ -0,0 +1,23 @@ +package configs; + +public class RemoteDataSettings { + private final String url; + private final String token; + + public RemoteDataSettings() { + this("", ""); + } + + public RemoteDataSettings(String url, String token) { + this.url = url; + this.token = token; + } + + public String getUrl() { + return url; + } + + public String getToken() { + return token; + } +} diff --git a/src/main/java/configs/Settings.java b/src/main/java/configs/Settings.java index d1705f6..ae1df3f 100644 --- a/src/main/java/configs/Settings.java +++ b/src/main/java/configs/Settings.java @@ -3,31 +3,55 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import roles.Player; import java.io.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; public class Settings { + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .create(); public static final Settings instance = new Settings(); - private static final Logger LOGGER = LogManager.getLogger(Settings.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Settings.class); private SettingsInstance settingsInstance; private Quotes quotes; private Settings() { - try { - settingsInstance = new Gson().fromJson(new BufferedReader(new FileReader("resources/settings.json")), new TypeToken() { - }.getType()); - quotes = new Gson().fromJson(new BufferedReader(new FileReader("resources/quotes.json")), new TypeToken() { - }.getType()); - } catch (FileNotFoundException e) { + String settingsFileLocation = Optional.ofNullable(System.getenv("SETTINGS_FILE")) + .orElse("resources/settings.json"); + try ( + FileReader fileReader = new FileReader(settingsFileLocation); + BufferedReader bufferedReader = new BufferedReader(fileReader) + ) { + settingsInstance = GSON.fromJson( + bufferedReader, + new TypeToken() { + }.getType() + ); + } catch (IOException e) { LOGGER.error("Unable to find settings file!"); settingsInstance = new SettingsInstance(); + } + String quotesFileLocation = Optional.ofNullable(System.getenv("QUOTES_FILE")) + .orElse("resources/quotes.json"); + try ( + FileReader fileReader = new FileReader(quotesFileLocation); + BufferedReader bufferedReader = new BufferedReader(fileReader) + ) { + quotes = GSON.fromJson( + bufferedReader, + new TypeToken() { + }.getType() + ); + } catch (IOException e) { + LOGGER.error("Unable to find quotes file!"); quotes = new Quotes(); } } @@ -48,6 +72,10 @@ public static Quotes getQuotes() { return instance.quotes; } + public static RemoteDataSettings getRemoteDataSettings() { + return getSettingsInstance().getRemoteDataSettings(); + } + private static SettingsInstance getSettingsInstance() { return instance.settingsInstance; } @@ -56,22 +84,28 @@ private static SettingsInstance getSettingsInstance() { * Serializes the values of the settings and writes it to settings.json. Generally used to update doom pools. */ public static void serializePersonalSettings() { - try { - final BufferedWriter writer = new BufferedWriter(new FileWriter("resources/settings.json")); - new GsonBuilder().setPrettyPrinting().create().toJson(Settings.getSettingsInstance(), writer); - writer.close(); + String settingsFileLocation = Optional.ofNullable(System.getenv("SETTINGS_FILE")) + .orElse("resources/settings.json"); + try ( + FileWriter fileWriter = new FileWriter(settingsFileLocation); + BufferedWriter writer = new BufferedWriter(fileWriter) + ) { + GSON.toJson(Settings.getSettingsInstance(), writer); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Unable to serialize settings file!", e); } } protected static void serializeQuotes() { - try { - final BufferedWriter writer = new BufferedWriter(new FileWriter("resources/quotes.json")); - new GsonBuilder().setPrettyPrinting().create().toJson(Settings.getQuotes(), writer); - writer.close(); + String quotesFileLocation = Optional.ofNullable(System.getenv("QUOTES_FILE")) + .orElse("resources/quotes.json"); + try ( + FileWriter fileWriter = new FileWriter(quotesFileLocation); + BufferedWriter writer = new BufferedWriter(fileWriter) + ) { + GSON.toJson(Settings.getQuotes(), writer); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Unable to serialize quotes file!", e); } } @@ -93,12 +127,14 @@ public static void removeQuote(String quote) { private static class SettingsInstance { private final DoomSettings doom; private final DiscordSettings discordSettings; + private final RemoteDataSettings remoteDataSettings; private final List players; public SettingsInstance() { doom = new DoomSettings(); discordSettings = new DiscordSettings(); players = new ArrayList<>(); + this.remoteDataSettings = new RemoteDataSettings(); } public DoomSettings getDoom() { @@ -112,5 +148,9 @@ public DiscordSettings getDiscordSettings() { public List getPlayers() { return players; } + + public RemoteDataSettings getRemoteDataSettings() { + return remoteDataSettings; + } } } diff --git a/src/main/java/discord/TwoDee.java b/src/main/java/discord/TwoDee.java index be1705a..e3dd248 100644 --- a/src/main/java/discord/TwoDee.java +++ b/src/main/java/discord/TwoDee.java @@ -1,27 +1,31 @@ package discord; import configs.Settings; +import doom.DoomHandler; import language.LanguageLogic; import listeners.DoomPoolAutocomplete; import listeners.LanguageAutocompleteListener; import listeners.PoolAutocompleteListener; import logic.AwardContextMenu; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.javacord.api.DiscordApiBuilder; import org.javacord.api.entity.intent.Intent; import org.javacord.api.util.logging.ExceptionLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import pw.mihou.velen.interfaces.Velen; import pw.mihou.velen.internals.observer.VelenObserver; import pw.mihou.velen.internals.observer.modes.ObserverMode; +import roles.PlayerHandler; import slashcommands.SlashCommandRegister; import java.io.File; public class TwoDee { - private static final Logger LOGGER = LogManager.getLogger(TwoDee.class); + private static final Logger LOGGER = LoggerFactory.getLogger(TwoDee.class); public static void main(String[] args) { + + DoomHandler.setupRemoteDoom(); String token = Settings.getDiscordSettings().getToken(); LanguageLogic languageLogic = LanguageLogic.of(new File("resources/languages.json")) @@ -31,17 +35,23 @@ public static void main(String[] args) { ); final Velen velen = SlashCommandRegister.setupVelen(languageLogic); - new DiscordApiBuilder().setToken(token).setAllIntentsExcept(Intent.GUILD_PRESENCES).setUserCacheEnabled(true).addListener(velen).login().thenAccept(api -> { - System.out.println("You can invite the bot by using the following url: " + api.createBotInvite() + "&scope=bot%20applications.commands"); + new DiscordApiBuilder() + .setToken(token) + .setAllIntentsExcept(Intent.GUILD_PRESENCES) + .setUserCacheEnabled(true) + .addListener(velen) + .login() + .thenCompose(PlayerHandler.getInstance()::uploadPlayersToRemote) + .thenAccept(api -> { + LOGGER.info("You can invite the bot by using the following url: {}&scope=bot%20applications.commands", api.createBotInvite()); velen.registerAllSlashCommands(api); //Send startup message Settings.getDiscordSettings().getAnnouncementChannels().forEach(id -> { var channel = api.getTextChannelById(id); if (channel.isPresent()) { channel.get().sendMessage(Settings.getQuotes().getRandomStartupQuote()); - } - else { - LOGGER.error("Failed to find channel for ID: %d".formatted(id)); + } else { + LOGGER.error("Failed to find channel for ID: {}", id); } }); AwardContextMenu.setupContextMenu(api); diff --git a/src/main/java/doom/ConfigDoom.java b/src/main/java/doom/ConfigDoom.java new file mode 100644 index 0000000..29f6d52 --- /dev/null +++ b/src/main/java/doom/ConfigDoom.java @@ -0,0 +1,44 @@ +package doom; + +import configs.Settings; + +import java.util.concurrent.CompletableFuture; + +public record ConfigDoom( + String name, + int doom +) implements DoomPool { + + @Override + public String getName() { + return this.name(); + } + + @Override + public CompletableFuture getDoom() { + return CompletableFuture.completedFuture(this.doom()); + } + + @Override + public CompletableFuture changeDoom(int amount) { + int old = this.doom(); + int current = this.doom() + amount; + DoomHandler.getInstance().doomSettings.getDoomPools() + .put(this.name(), current); + return CompletableFuture.completedFuture(new DoomChange(old, current)); + } + + @Override + public CompletableFuture delete() { + Integer removed = DoomHandler.getInstance() + .doomSettings + .getDoomPools() + .remove(this.getName()); + if (removed == null) { + return CompletableFuture.completedFuture(false); + } else { + Settings.serializePersonalSettings(); + return CompletableFuture.completedFuture(true); + } + } +} diff --git a/src/main/java/doom/DoomChange.java b/src/main/java/doom/DoomChange.java new file mode 100644 index 0000000..85a2055 --- /dev/null +++ b/src/main/java/doom/DoomChange.java @@ -0,0 +1,7 @@ +package doom; + +public record DoomChange( + int old, + int current +) { +} diff --git a/src/main/java/doom/DoomHandler.java b/src/main/java/doom/DoomHandler.java index 5fb5a7c..96d4c22 100644 --- a/src/main/java/doom/DoomHandler.java +++ b/src/main/java/doom/DoomHandler.java @@ -1,34 +1,106 @@ package doom; import com.google.common.collect.Lists; +import com.google.gson.Gson; import configs.DoomSettings; import configs.Settings; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.apache.commons.lang3.tuple.Pair; import org.javacord.api.entity.message.embed.EmbedBuilder; import org.javacord.api.entity.user.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import roles.Player; import roles.PlayerHandler; import roles.Storytellers; +import util.CachedValue; import util.DamerauLevenshtein; import javax.annotation.Nullable; import java.awt.*; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.text.MessageFormat; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; public final class DoomHandler { - private static final Logger LOGGER = LogManager.getLogger(DoomHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DoomHandler.class); public static final String DOOM = "Doom!"; + private static final Gson GSON = new Gson(); private static final DoomHandler instance = new DoomHandler(); - private final DoomSettings doomSettings; + public final DoomSettings doomSettings; + + private final CachedValue> remoteDooms; private DoomHandler() { doomSettings = Settings.getDoom(); + remoteDooms = new CachedValue<>(Duration.ofHours(4).toMillis(), this::getRemoteDoom); + } + + public static void setupRemoteDoom() { + if (!getOldDoomPools().isEmpty()) { + instance.doomSettings + .getDoomPools() + .entrySet() + .removeIf(doomPool -> createDoomPool(doomPool.getKey(), doomPool.getValue())); + Settings.serializePersonalSettings(); + } + + instance.remoteDooms.initialize(); + } + + private static boolean createDoomPool(String name, int count) { + HttpClient httpClient = HttpClient.newHttpClient(); + + String body = GSON.toJson(new RemoteDoomRequest(name, count)); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Settings.getRemoteDataSettings().getUrl() + "/doompool")) + .header("Authorization", "Bearer " + Settings.getRemoteDataSettings().getToken()) + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + try { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + LOGGER.info("Wrote Remote Doom: {}", response.body()); + if (response.statusCode() == 200) { + return true; + } else { + LOGGER.error("Received {}, {}", response.statusCode(), response.body()); + return false; + } + } catch (Exception e) { + LOGGER.error("Failed to create doom pool", e); + return false; + } + + } + + private List getRemoteDoom() { + try { + HttpClient httpClient = HttpClient.newHttpClient(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Settings.getRemoteDataSettings().getUrl() + "/doompool")) + .header("Authorization", "Bearer " + Settings.getRemoteDataSettings().getToken()) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return GSON.fromJson(response.body(), RemoteDoomPool.LIST_TYPE_TOKEN.getType()); + } catch (Exception e) { + LOGGER.error("Failed to read Remote Doom", e); + return Collections.emptyList(); + } } /** @@ -40,15 +112,24 @@ private DoomHandler() { * @return An embed that shows the change in doom points for the specified doom pool */ public static EmbedBuilder addDoom(String pool, int doomVal) { - final int oldDoom = getDoom(pool); - if (oldDoom == 0) { - return new EmbedBuilder() - .setTitle("Error") - .setDescription("No Doom Pool with Name ''**" + pool + "**'' exists."); - } - final int newDoom = getDoomPools().compute(pool, (s, integer) -> integer != null ? integer + doomVal : doomVal); - Settings.serializePersonalSettings(); - return generateDoomEmbed(pool, oldDoom, newDoom); + return getDoomPool(pool) + .map(doomPool -> { + try { + DoomChange doomChange = doomPool.changeDoom(doomVal).get(); + return generateDoomEmbed(pool, doomChange.old(), doomChange.current()); + } catch (InterruptedException | ExecutionException e) { + LOGGER.error("Failed to update doom", e); + return new EmbedBuilder() + .setTitle("Error") + .setDescription("Failed to update doom: " + e.getMessage()); + } + }) + .orElseGet(() -> new EmbedBuilder() + .setTitle("Error") + .setDescription("No Doom Pool with Name ''**" + pool + "**'' exists.") + ); + + } /** @@ -72,8 +153,7 @@ public static int addDoom(int doomVal) { public static int addDoomOnOpportunity(User user, int count) { if (Storytellers.isUserStoryteller(user)) { return getDoom(); - } - else { + } else { final String doomPool = PlayerHandler.getPlayerFromUser(user).map(Player::getDoomPool).orElse(getActivePool()); addDoom(doomPool, count); return getDoom(doomPool); @@ -87,7 +167,42 @@ public static int addDoomOnOpportunity(User user, int count) { * @return The number of doom points in that pool */ public static int getDoom(String pool) { - return getDoomPools().getOrDefault(pool, 0); + return getDoomPool(pool) + .flatMap(doomPool -> { + try { + return Optional.of(doomPool.getDoom().get()); + } catch (InterruptedException | ExecutionException e) { + LOGGER.error("Failed to call getDoom(String pool)", e); + return Optional.empty(); + } + }) + .orElse(0); + } + + public static Optional getDoomPool(String pool) { + return getDoomPools() + .stream() + .filter(doomPool -> doomPool.getName().equalsIgnoreCase(pool)) + .findFirst(); + } + + public static List getRemoteDoomPools() { + return instance.remoteDooms.get(); + } + + public static List getOldDoomPools() { + return instance.doomSettings.getDoomPools() + .entrySet() + .stream() + .map(entry -> new ConfigDoom(entry.getKey(), entry.getValue())) + .toList(); + } + + public static List getDoomPools() { + ArrayList doomPools = new ArrayList<>(); + doomPools.addAll(getRemoteDoomPools()); + doomPools.addAll(getOldDoomPools()); + return doomPools; } /** @@ -105,23 +220,34 @@ public static int getDoom() { * @param newDoom The new value of the doom pool * @return The embed with a new doom value */ - public static EmbedBuilder setDoom(String pool, int newDoom) { - int oldDoom = getDoom(pool); - if (oldDoom == 0) { + public static EmbedBuilder setDoom(String poolName, int newDoom) { + try { + return setDoomAsync(poolName, newDoom).get(); + } catch (InterruptedException | ExecutionException e) { + LOGGER.error("Failed to set doom", e); return new EmbedBuilder() .setTitle("Error") - .setDescription("No Doom Pool with Name ''**" + pool + "**'' exists."); + .setDescription("Failed to set doom for ''**" + poolName + "**'':" + e.getMessage()); } - getDoomPools().put(pool, newDoom); - Settings.serializePersonalSettings(); - return generateDoomEmbed(pool, oldDoom, newDoom); + } + + public static CompletableFuture setDoomAsync(String poolName, int newDoom) { + return getDoomPool(poolName) + .map(pool -> pool.getDoom() + .thenCompose(doom -> pool.changeDoom(newDoom - doom)) + .thenApply(doomChange -> generateDoomEmbed(pool.getName(), doomChange.old(), doomChange.current())) + ) + .orElseGet(() -> CompletableFuture.completedFuture(new EmbedBuilder() + .setTitle("Error") + .setDescription("No Doom Pool with Name ''**" + poolName + "**'' exists.") + )); } /** * Generates an embed that shows the amount of doom points in the specified doom pool. * * @param pool The name of the doom pool to check - * @return The an embed containing the value of the doom pool + * @return The embed containing the value of the doom pool */ public static EmbedBuilder generateDoomEmbed(String pool) { final int doom = getDoom(pool); @@ -154,28 +280,43 @@ public static EmbedBuilder generateDoomEmbed(String pool, int oldDoom, int newDo * @return An embed containing information about the deleted doom pool and the number of doom points it had */ public static EmbedBuilder deletePool(String pool) { - final Integer removedDoom = getDoomPools().remove(pool); - final String description = Optional.ofNullable(removedDoom).map(doom -> MessageFormat.format("I''ve removed the ''**{0}**'' doom pool, which contained {1} doom points.", pool, doom)).orElseGet(() -> MessageFormat.format("I was unable to find the ''**{0}**'' doom pool", pool)); - Settings.serializePersonalSettings(); + String description = getDoomPool(pool) + .map(doomPool -> MessageFormat.format("I''ve removed the ''**{0}**'' doom pool, which contained {1} doom points.", pool, doomPool)) + .orElseGet(() -> MessageFormat.format("I was unable to find the ''**{0}**'' doom pool", pool)); + return new EmbedBuilder() .setTitle(DOOM) .setDescription(description); } /** - * Generates an embed that provides a summary of all of the doom pools. + * Generates an embed that provides a summary of the doom pools. * * @return Am embed with information on the names and values of all available doom pools and the name and value of * the active doom pool */ - public static EmbedBuilder generateDoomEmbed() { - final EmbedBuilder embedBuilder = new EmbedBuilder().setTitle(DOOM).setDescription(MessageFormat.format("Here are the values of all doom pools.\nThe current active doom pool is ''**{0}**'' with {1} doom points", getActivePool(), getDoom(getActivePool()))).setColor(new Color((int) (getDoom() % 101 * (2.55)))); - getDoomPools().forEach((key, value) -> embedBuilder.addField(key, String.valueOf(value))); - return embedBuilder; - } + public static CompletableFuture generateDoomEmbed() { + final EmbedBuilder embedBuilder = new EmbedBuilder() + .setTitle(DOOM) + .setDescription(MessageFormat.format( + "Here are the values of all doom pools.\nThe current active doom pool is ''**{0}**'' with {1} doom points", + getActivePool(), + getDoom(getActivePool()) + )) + .setColor(new Color((int) (getDoom() % 101 * (2.55)))); + List>> futures = new ArrayList<>(); + for (DoomPool doomPool : getDoomPools()) { + futures.add(doomPool.getDoom().thenApply(doom -> Pair.of(doomPool.getName(), doom))); + } + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> { + futures.forEach(pairFuture -> { + Pair pair = pairFuture.join(); + embedBuilder.addField(pair.getLeft(), String.valueOf(pair.getRight())); + }); - public static Map getDoomPools() { - return instance.doomSettings.getDoomPools(); + return embedBuilder; + }); } /** @@ -208,49 +349,52 @@ public static String getDoomPoolOrDefault(User user) { } public static EmbedBuilder createPool(String poolName, int count) { - getDoomPools().put(poolName, count); - Settings.serializePersonalSettings(); - return new EmbedBuilder() - .setTitle(DOOM) - .setDescription(MessageFormat.format("I''ve created the doom pool ''**{0}**'', which contains {1} doom points.", poolName, count)); + if (createDoomPool(poolName, count)) { + return new EmbedBuilder() + .setTitle(DOOM) + .setDescription(MessageFormat.format("I''ve created the doom pool ''**{0}**'', which contains {1} doom points.", poolName, count)); + } else { + return new EmbedBuilder() + .setTitle(DOOM) + .setDescription(MessageFormat.format("Failed to create doom pool ''**{0}**''.", poolName)); + } } @Nullable public static String findPool(String poolName) { - if (getDoomPools().containsKey(poolName)) { + if (getDoomPools().stream().anyMatch(pool -> pool.getName().equalsIgnoreCase(poolName))) { return poolName; - } - else { + } else { List potentialPoolNames = Lists.newArrayList(); int currentDistance = Integer.MAX_VALUE; - for (String existingPool : getDoomPools().keySet()) { - int distance = DamerauLevenshtein.calculateDistance(poolName, existingPool); + for (DoomPool existingPool : getDoomPools()) { + int distance = DamerauLevenshtein.calculateDistance(poolName, existingPool.getName()); if (distance < currentDistance) { potentialPoolNames.clear(); - potentialPoolNames.add(existingPool); + potentialPoolNames.add(existingPool.getName()); currentDistance = distance; - } - else if (distance == currentDistance) { - potentialPoolNames.add(existingPool); + } else if (distance == currentDistance) { + potentialPoolNames.add(existingPool.getName()); } } if (potentialPoolNames.isEmpty()) { return null; - } - else if (potentialPoolNames.size() == 1) { + } else if (potentialPoolNames.size() == 1) { if (currentDistance <= 2) { return potentialPoolNames.get(0); - } - else { + } else { return null; } - } - else { + } else { LOGGER.warn("Found multiple doom pool names with same level of similarity"); return null; } } } + public static DoomHandler getInstance() { + return instance; + } + } \ No newline at end of file diff --git a/src/main/java/doom/DoomPool.java b/src/main/java/doom/DoomPool.java new file mode 100644 index 0000000..8e28183 --- /dev/null +++ b/src/main/java/doom/DoomPool.java @@ -0,0 +1,13 @@ +package doom; + +import java.util.concurrent.CompletableFuture; + +public interface DoomPool { + String getName(); + + CompletableFuture getDoom(); + + CompletableFuture changeDoom(int amount); + + CompletableFuture delete(); +} diff --git a/src/main/java/doom/RemoteDoomPool.java b/src/main/java/doom/RemoteDoomPool.java new file mode 100644 index 0000000..450b04f --- /dev/null +++ b/src/main/java/doom/RemoteDoomPool.java @@ -0,0 +1,100 @@ +package doom; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import configs.Settings; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public record RemoteDoomPool( + long id, + String name, + int doom +) implements DoomPool { + public static final TypeToken> LIST_TYPE_TOKEN = new TypeToken<>() { + }; + + private static final Gson GSON = new Gson(); + private static final HttpClient httpClient = HttpClient.newHttpClient(); + + @Override + public String getName() { + return this.name(); + } + + @Override + public CompletableFuture getDoom() { + return httpClient.sendAsync( + HttpRequest.newBuilder() + .GET() + .uri(URI.create(Settings.getRemoteDataSettings().getUrl() + "/doompool/" + this.id())) + .header("Authorization", "Bearer " + Settings.getRemoteDataSettings().getToken()) + .build(), + BodyHandlers.ofString() + ).thenComposeAsync(httpResponse -> { + if (httpResponse.statusCode() == 200) { + try { + RemoteDoomPool doomPool = GSON.fromJson(httpResponse.body(), RemoteDoomPool.class); + return CompletableFuture.completedFuture(doomPool.doom()); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } else { + return CompletableFuture.failedFuture( + new Exception("Received %s for Http Response for getDoom".formatted(httpResponse.statusCode())) + ); + } + }); + } + + @Override + public CompletableFuture changeDoom(int amount) { + return httpClient.sendAsync( + HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(Map.of("amount", amount)))) + .uri(URI.create(Settings.getRemoteDataSettings().getUrl() + "/doompool/" + this.id() + "/doom")) + .header("Authorization", "Bearer " + Settings.getRemoteDataSettings().getToken()) + .build(), + BodyHandlers.ofString() + ).thenComposeAsync(httpResponse -> { + if (httpResponse.statusCode() == 200) { + try { + DoomChange doomChange = GSON.fromJson(httpResponse.body(), DoomChange.class); + return CompletableFuture.completedFuture(doomChange); + } catch (Exception e) { + return CompletableFuture.failedFuture(e); + } + } else { + return CompletableFuture.failedFuture( + new Exception("Received %s for Http Response for getDoom".formatted(httpResponse.statusCode())) + ); + } + }); + } + + @Override + public CompletableFuture delete() { + return httpClient.sendAsync( + HttpRequest.newBuilder() + .DELETE() + .uri(URI.create(Settings.getRemoteDataSettings().getUrl() + "/doompool/" + this.id())) + .header("Authorization", "Bearer " + Settings.getRemoteDataSettings().getToken()) + .build(), + BodyHandlers.ofString() + ).thenComposeAsync(httpResponse -> { + if (httpResponse.statusCode() == 200) { + return CompletableFuture.completedFuture(true); + } else { + return CompletableFuture.failedFuture( + new Exception("Received %s for Http Response for getDoom".formatted(httpResponse.statusCode())) + ); + } + }); + } +} diff --git a/src/main/java/doom/RemoteDoomRequest.java b/src/main/java/doom/RemoteDoomRequest.java new file mode 100644 index 0000000..b39d99e --- /dev/null +++ b/src/main/java/doom/RemoteDoomRequest.java @@ -0,0 +1,7 @@ +package doom; + +public record RemoteDoomRequest( + String name, + int doom +) { +} diff --git a/src/main/java/language/LanguageLogic.java b/src/main/java/language/LanguageLogic.java index bd03fd5..296d863 100644 --- a/src/main/java/language/LanguageLogic.java +++ b/src/main/java/language/LanguageLogic.java @@ -8,11 +8,11 @@ import com.google.gson.*; import io.vavr.control.Try; import org.apache.commons.lang3.tuple.Pair; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.jgrapht.Graph; import org.jgrapht.alg.DijkstraShortestPath; import org.jgrapht.graph.guava.MutableGraphAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import util.DamerauLevenshtein; import util.GsonHelper; import util.Match; @@ -30,7 +30,7 @@ @SuppressWarnings("UnstableApiUsage") public class LanguageLogic { - private static final Logger LOGGER = LogManager.getLogger(LanguageLogic.class); + private static final Logger LOGGER = LoggerFactory.getLogger(LanguageLogic.class); private static final Gson GSON = new GsonBuilder() .setPrettyPrinting() .create(); diff --git a/src/main/java/listeners/DoomPoolAutocomplete.java b/src/main/java/listeners/DoomPoolAutocomplete.java index bfcd535..43bb0ec 100644 --- a/src/main/java/listeners/DoomPoolAutocomplete.java +++ b/src/main/java/listeners/DoomPoolAutocomplete.java @@ -15,9 +15,9 @@ public class DoomPoolAutocomplete implements AutocompleteCreateListener { public void onAutocompleteCreate(AutocompleteCreateEvent event) { if (event.getAutocompleteInteraction().getFocusedOption().getName().equals(DoomLogic.POOL_NAME)) { final String enteredValue = event.getAutocompleteInteraction().getFocusedOption().getStringValue().orElse(""); - final List choices = DoomHandler.getDoomPools().keySet().stream() - .filter(poolName -> UtilFunctions.containsIgnoreCase(poolName, enteredValue)) - .map(s -> SlashCommandOptionChoice.create(s, s)) + final List choices = DoomHandler.getDoomPools().stream() + .filter(pool -> UtilFunctions.containsIgnoreCase(pool.getName(), enteredValue)) + .map(s -> SlashCommandOptionChoice.create(s.getName(), s.getName())) .collect(Collectors.toList()); event.getAutocompleteInteraction().respondWithChoices(choices); } diff --git a/src/main/java/listeners/RollComponentInteractionListener.java b/src/main/java/listeners/RollComponentInteractionListener.java index 3e4b31d..8bcd688 100644 --- a/src/main/java/listeners/RollComponentInteractionListener.java +++ b/src/main/java/listeners/RollComponentInteractionListener.java @@ -202,8 +202,13 @@ private void handleReroll(ButtonInteraction interaction, Message interactionMess * @return A completable future indicating that the plot points and doom points have been rolled back */ private CompletableFuture rollBackChanges() { - PlayerHandler.getPlayerFromUser(user).map(Player::getDoomPool).ifPresent(s -> DoomHandler.setDoom(s, originalDoomPoints)); - return SheetsHandler.setPlotPoints(user, originalPlotPoints).thenAccept(integer -> {}); + return CompletableFuture.allOf( + PlayerHandler.getPlayerFromUser(user) + .map(Player::getDoomPool) + .map(s -> DoomHandler.setDoomAsync(s, originalDoomPoints)) + .orElseGet(() -> CompletableFuture.completedFuture(null)), + SheetsHandler.setPlotPoints(user, originalPlotPoints) + ); } /** diff --git a/src/main/java/logic/CalculationLogic.java b/src/main/java/logic/CalculationLogic.java index 731519b..dd66563 100644 --- a/src/main/java/logic/CalculationLogic.java +++ b/src/main/java/logic/CalculationLogic.java @@ -9,8 +9,6 @@ import exceptions.UserException; import io.vavr.Tuple; import io.vavr.control.Try; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.javacord.api.entity.channel.Channel; import org.javacord.api.entity.channel.ChannelType; import org.javacord.api.entity.channel.TextChannel; @@ -20,6 +18,8 @@ import org.javacord.api.event.message.MessageCreateEvent; import org.javacord.api.interaction.*; import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import pw.mihou.velen.interfaces.*; import pw.mihou.velen.interfaces.routed.VelenRoutedOptions; @@ -34,7 +34,7 @@ * Command Class for Calculation. Handles IO, as well as streaming all messages into the Collector */ public class CalculationLogic implements VelenEvent, VelenSlashEvent { - private final static Logger LOGGER = LogManager.getLogger(CalculationLogic.class); + private final static Logger LOGGER = LoggerFactory.getLogger(CalculationLogic.class); @SuppressWarnings("ResultOfMethodCallIgnored") @Override diff --git a/src/main/java/logic/DoomLogic.java b/src/main/java/logic/DoomLogic.java index 6d41eff..ce56e05 100644 --- a/src/main/java/logic/DoomLogic.java +++ b/src/main/java/logic/DoomLogic.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; public class DoomLogic implements VelenHybridHandler { @@ -73,38 +74,29 @@ private static SlashCommandOptionBuilder getCountOption() { .setRequired(false); } - private EmbedBuilder handleCommand(String mode, String poolName, int count) { + private CompletableFuture handleCommand(String mode, String poolName, int count) { String actualPoolName = DoomHandler.findPool(poolName); if (actualPoolName == null) { if (mode == null || mode.isEmpty() || mode.equalsIgnoreCase("list")) { return DoomHandler.generateDoomEmbed(); - } - else { - return new EmbedBuilder() + } else { + return CompletableFuture.completedFuture(new EmbedBuilder() .setTitle("Error") - .setDescription("No Doom Pool with Name ''**" + poolName + "**'' exists."); - } - } - else { - switch (mode) { - case "add": - return DoomHandler.addDoom(actualPoolName, count); - case "sub": - return DoomHandler.addDoom(actualPoolName, count * -1); - case "select": - return DoomHandler.setActivePool(actualPoolName); - case "set": - return DoomHandler.setDoom(actualPoolName, count); - case "delete": - return DoomHandler.deletePool(actualPoolName); - case "create": - return DoomHandler.createPool(actualPoolName, count); - case "list": - return DoomHandler.generateDoomEmbed(); - case "query": - default: - return actualPoolName.equals("") ? DoomHandler.generateDoomEmbed() : DoomHandler.generateDoomEmbed(actualPoolName); + .setDescription("No Doom Pool with Name ''**" + poolName + "**'' exists.") + ); } + } else { + return switch (mode) { + case "add" -> CompletableFuture.completedFuture(DoomHandler.addDoom(actualPoolName, count)); + case "sub" -> CompletableFuture.completedFuture(DoomHandler.addDoom(actualPoolName, count * -1)); + case "select" -> CompletableFuture.completedFuture(DoomHandler.setActivePool(actualPoolName)); + case "set" -> CompletableFuture.completedFuture(DoomHandler.setDoom(actualPoolName, count)); + case "delete" -> CompletableFuture.completedFuture(DoomHandler.deletePool(actualPoolName)); + case "create" -> CompletableFuture.completedFuture(DoomHandler.createPool(actualPoolName, count)); + case "list" -> DoomHandler.generateDoomEmbed(); + default -> + actualPoolName.isEmpty() ? DoomHandler.generateDoomEmbed() : CompletableFuture.completedFuture(DoomHandler.generateDoomEmbed(actualPoolName)); + }; } } @@ -120,7 +112,8 @@ public void onEvent(VelenGeneralEvent event, VelenGeneralResponder responder, Us final String name = subcommand.flatMap(velenSubcommand -> velenSubcommand.withName(POOL_NAME)) .flatMap(VelenOption::asString) .orElse(DoomHandler.getActivePool()); - responder.addEmbed(handleCommand(subcommandName, name, count)).respond(); + handleCommand(subcommandName, name, count) + .thenCompose(embed -> responder.addEmbed(embed).respond()); } } } diff --git a/src/main/java/roles/PlayerHandler.java b/src/main/java/roles/PlayerHandler.java index 779b6a0..2e12f37 100644 --- a/src/main/java/roles/PlayerHandler.java +++ b/src/main/java/roles/PlayerHandler.java @@ -1,18 +1,31 @@ package roles; +import com.google.gson.Gson; import configs.Settings; +import doom.DoomHandler; +import doom.RemoteDoomPool; import org.apache.commons.lang3.tuple.Pair; +import org.javacord.api.DiscordApi; import org.javacord.api.entity.Mentionable; import org.javacord.api.entity.permission.Role; import org.javacord.api.entity.user.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sheets.SheetsHandler; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.stream.Collectors; public class PlayerHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(PlayerHandler.class); + private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + private static final Gson GSON = new Gson(); private static final PlayerHandler instance = new PlayerHandler(); private final List players; @@ -40,14 +53,12 @@ public static Collection> getPlayersFromMentionable(Mentionab ) .flatMap(Optional::stream) .collect(Collectors.toSet()); - } - else if (mentionable instanceof User) { + } else if (mentionable instanceof User) { return getPlayerFromUser((User) mentionable) .map(player -> Pair.of((User) mentionable, player)) .map(Collections::singleton) .orElse(Collections.emptySet()); - } - else { + } else { return Collections.emptyList(); } } @@ -55,4 +66,115 @@ else if (mentionable instanceof User) { public List getPlayers() { return players; } + + public CompletableFuture uploadPlayersToRemote(DiscordApi discordApi) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Settings.getRemoteDataSettings().getUrl() + "/character")) + .header("Authorization", "Bearer " + Settings.getRemoteDataSettings().getToken()) + .GET() + .build(); + + return HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .>thenApply(response -> GSON.fromJson( + response.body(), + RemoteCharacter.LIST_TYPE_TOKEN.getType() + )) + .thenApply(this::getPlayersWithoutRemoteCharacters) + .thenCompose(players -> CompletableFuture.allOf(players.stream() + .map(player -> createCharacterFor(discordApi, player)) + .toArray(CompletableFuture[]::new) + )) + .thenApply(ignore -> discordApi); + } + + private List getPlayersWithoutRemoteCharacters(List remoteCharacters) { + List existingSheets = remoteCharacters.stream() + .map(RemoteCharacter::sheet) + .toList(); + + return players.stream() + .filter(player -> !existingSheets.contains(player.getSheetId())) + .toList(); + } + + private CompletableFuture createCharacterFor(DiscordApi discordApi, Player player) { + return this.findUserForPlayer(Long.toString(player.getDiscordId())) + .thenCompose(existingUser -> existingUser.map(CompletableFuture::completedFuture) + .orElseGet(() -> this.createUserForPlayer(discordApi, player)) + ) + .thenCompose(remoteUser -> createCharacterFor(remoteUser, player)) + .thenAccept(remoteCharacter -> LOGGER.info("Created new Remote Character {}", remoteCharacter.name())); + } + + private CompletableFuture createCharacterFor(RemoteUser remoteUser, Player player) { + LOGGER.info("Creating new character for user {}", player.getDiscordId()); + return SheetsHandler.getName(player.getSheetId()) + .thenCompose(name -> { + if (!name.isEmpty()) { + Map map = new HashMap<>(); + + map.put("name", name); + map.put("sheet", player.getSheetId()); + map.put("ownerId", remoteUser.id()); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Settings.getRemoteDataSettings().getUrl() + "/character")) + .header("Authorization", "Bearer " + Settings.getRemoteDataSettings().getToken()) + .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(map))) + .build(); + + return HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(response -> GSON.fromJson(response.body(), RemoteCharacter.class)); + } else { + throw new CompletionException(new IllegalStateException("Failed to get name for " + player.getSheetId())); + } + }); + } + + private CompletableFuture> findUserForPlayer(String discordId) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Settings.getRemoteDataSettings().getUrl() + "/user?external_id=" + discordId)) + .header("Authorization", "Bearer " + Settings.getRemoteDataSettings().getToken()) + .GET() + .build(); + + return HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .>thenApply(response -> GSON.fromJson(response.body(), RemoteUser.LIST_TYPE_TOKEN.getType())) + .thenApply(existingUsers -> existingUsers.stream() + .filter(user -> Optional.ofNullable(user.externalId()) + .filter(discordId::equals) + .isPresent() + ) + .findFirst() + ); + } + + private CompletableFuture createUserForPlayer(DiscordApi discordApi, Player player) { + Optional doomPoolId = DoomHandler.getDoomPool(player.getDoomPool()) + .flatMap(doomPool -> { + if (doomPool instanceof RemoteDoomPool remoteDoomPool) { + return Optional.of(remoteDoomPool.id()); + } else { + return Optional.empty(); + } + }); + LOGGER.info("Creating new user for user {}", player.getDiscordId()); + return discordApi.getUserById(player.getDiscordId()) + .thenCompose(user -> { + Map requestValues = new HashMap<>(); + requestValues.put("name", user.getName()); + requestValues.put("externalId", Long.toString(player.getDiscordId())); + requestValues.put("notes", "Created by TwoDee"); + doomPoolId.ifPresent(id -> requestValues.put("doomPermission", id.toString())); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Settings.getRemoteDataSettings().getUrl() + "/user")) + .header("Authorization", "Bearer " + Settings.getRemoteDataSettings().getToken()) + .POST(HttpRequest.BodyPublishers.ofString(GSON.toJson(requestValues))) + .build(); + + return HTTP_CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()); + }) + .thenApply(response -> GSON.fromJson(response.body(), RemoteUser.class)); + } } diff --git a/src/main/java/roles/RemoteCharacter.java b/src/main/java/roles/RemoteCharacter.java new file mode 100644 index 0000000..e125c89 --- /dev/null +++ b/src/main/java/roles/RemoteCharacter.java @@ -0,0 +1,18 @@ +package roles; + +import com.google.gson.reflect.TypeToken; + +import java.util.List; +import java.util.Map; + +public record RemoteCharacter( + long id, + String name, + String sheet, + long ownerId, + int plotPoints, + Map skills +) { + public static final TypeToken> LIST_TYPE_TOKEN = new TypeToken<>() { + }; +} diff --git a/src/main/java/roles/RemoteUser.java b/src/main/java/roles/RemoteUser.java new file mode 100644 index 0000000..22510c6 --- /dev/null +++ b/src/main/java/roles/RemoteUser.java @@ -0,0 +1,22 @@ +package roles; + +import com.google.gson.reflect.TypeToken; + +import javax.annotation.Nullable; +import java.util.List; + +public record RemoteUser( + long id, + String name, + String characterPermission, + String doomPermission, + String userPermission, + String tokenPermission, + @Nullable String externalId, + String notes, + boolean active, + @Nullable Long createdBy +) { + public static final TypeToken> LIST_TYPE_TOKEN = new TypeToken<>() { + }; +} diff --git a/src/main/java/sheets/SheetsHandler.java b/src/main/java/sheets/SheetsHandler.java index 80f6e1e..8b1f51d 100644 --- a/src/main/java/sheets/SheetsHandler.java +++ b/src/main/java/sheets/SheetsHandler.java @@ -19,6 +19,8 @@ import org.apache.commons.lang3.tuple.Pair; import org.javacord.api.entity.user.User; import org.javacord.api.interaction.SlashCommandOptionChoice; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import roles.Player; import roles.PlayerHandler; @@ -30,9 +32,11 @@ import java.text.MessageFormat; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.stream.Collectors; public class SheetsHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(SheetsHandler.class); private static final String APPLICATION_NAME = "Skill Lookup"; private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); private static final String TOKENS_DIRECTORY_PATH = "tokens"; @@ -117,7 +121,7 @@ private static Optional getPlotPointRange(User user) { try { return Optional.of(instance.service.spreadsheets().values().get(spreadsheetForUser.get(), "PlotPoints").execute()); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Error while reading plot point range for user {}", spreadsheetForUser, e); } } return Optional.empty(); @@ -159,7 +163,7 @@ public static CompletableFuture> setPlotPoints(User user, int final int updatedPlotPointCount = Integer.parseInt((String) plotPointsCellUpdateRequest.getUpdatedData().getValues().get(0).get(0)); return Optional.of(updatedPlotPointCount); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Error while setting plot points for user {}", user, e); } return Optional.empty(); } @@ -167,16 +171,6 @@ public static CompletableFuture> setPlotPoints(User user, int }); } - /** - * A functon used to throw an exception. Used in completable futures to allow exceptions to be handled in exceptonally - * - * @throws T The exception to throw - */ - @SuppressWarnings("all") - public static R sneakyThrow(Throwable t) throws T { - throw (T) t; - } - /** * Gets the current bleed value of a player @@ -190,7 +184,7 @@ public static Optional getPlayerBleed(User user) { try { return Optional.of(instance.service.spreadsheets().values().get(spreadsheetForUser.get(), "PlotPointBleed").execute()).map(valueRange -> Integer.parseInt((String) valueRange.getValues().get(0).get(0))); } catch (IOException e) { - e.printStackTrace(); + LOGGER.error("Error while reading player bleed for user {}", user, e); } } return Optional.empty(); @@ -300,4 +294,20 @@ private static ValueRange getRange(String spreadsheetID, String range) throws IO .get(spreadsheetID, range) .execute(); } + + public static CompletableFuture getName(String spreadsheetId) { + return CompletableFuture.supplyAsync(() -> { + try { + ValueRange valueRange = getRange(spreadsheetId, "B1:L2"); + return valueRange.getValues() + .stream() + .flatMap(List::stream) + .findFirst() + .map(Object::toString) + .orElse(""); + } catch (IOException e) { + throw new CompletionException(e); + } + }); + } } diff --git a/src/main/java/util/CachedValue.java b/src/main/java/util/CachedValue.java new file mode 100644 index 0000000..a6d217a --- /dev/null +++ b/src/main/java/util/CachedValue.java @@ -0,0 +1,42 @@ +package util; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public class CachedValue { + private final long ttlMillis; + private final Supplier refresh; + + private final AtomicLong expiryTime; + private final AtomicReference value; + + public CachedValue(long ttlMillis, Supplier refresh) { + this.ttlMillis = ttlMillis; + this.refresh = refresh; + this.value = new AtomicReference<>(); + this.expiryTime = new AtomicLong(); + } + + public T get() { + if (expiryTime.get() < System.currentTimeMillis()) { + this.value.set(null); + } + + T currentValue = this.value.get(); + if (currentValue == null) { + currentValue = refresh.get(); + this.value.set(currentValue); + this.expiryTime.set(System.currentTimeMillis() + ttlMillis); + } + return currentValue; + } + + public void initialize() { + this.get(); + } + + public void forceRefresh() { + this.value.set(null); + } +} diff --git a/src/main/java/util/DamerauLevenshtein.java b/src/main/java/util/DamerauLevenshtein.java index 79a3f75..df46874 100644 --- a/src/main/java/util/DamerauLevenshtein.java +++ b/src/main/java/util/DamerauLevenshtein.java @@ -1,16 +1,15 @@ package util; import com.google.common.collect.Lists; -import doom.DoomHandler; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.List; import java.util.Optional; public class DamerauLevenshtein { - private static final Logger LOGGER = LogManager.getLogger(DamerauLevenshtein.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DamerauLevenshtein.class); /** * Calculates the string distance between source and target strings using diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml deleted file mode 100644 index 4a20803..0000000 --- a/src/main/resources/log4j2.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file