diff --git a/build.gradle.kts b/build.gradle.kts index b29b431..ddcdc4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("java") id("com.github.johnrengelman.shadow") version "7.1.0" id("maven-publish") + id("org.springframework.boot") version("2.7.8") } group = "com.alttd" @@ -34,12 +35,16 @@ tasks { withType { manifest { - attributes["Main-Class"] = "${rootProject.group}.${project.name}" +// attributes["Main-Class"] = "BOOT-INF/classes/${rootProject.group}.${project.name}" + attributes["Main-Class"] = "org.springframework.boot.loader.JarLauncher" } } shadowJar { archiveFileName.set(rootProject.name + ".jar") + manifest { + attributes["Main-Class"] = "org.springframework.boot.loader.JarLauncher" + } } build { @@ -54,7 +59,7 @@ tasks { dependencies { // JDA - implementation("net.dv8tion:JDA:5.0.0-beta.19") { + implementation("net.dv8tion:JDA:5.0.2") { exclude("opus-java") // exclude audio } // MySQL @@ -70,4 +75,7 @@ dependencies { compileOnly("org.projectlombok:lombok:1.18.30") annotationProcessor("org.projectlombok:lombok:1.18.24") implementation("com.alttd:AltitudeLogs:1.0") + implementation("org.springframework.boot:spring-boot-starter-web:3.2.1") + implementation("org.springframework.boot:spring-boot-starter-validation:3.2.1") + implementation("com.google.code.gson:gson:2.8.9") } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180..d64cd49 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 69a9715..1af9e09 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 744e882..1aa94a4 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,99 @@ # ############################################################################## -## -## 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"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | 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 +119,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,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -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 - -# 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 - # 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\"" - fi - i=`expr $i + 1` - 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" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# 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, 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. -# 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" +# 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 + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -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. +# + +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 ac1b06f..6689b85 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/src/main/java/com/alttd/AltitudeBot.java b/src/main/java/com/alttd/AltitudeBot.java index 555e18e..b46811e 100644 --- a/src/main/java/com/alttd/AltitudeBot.java +++ b/src/main/java/com/alttd/AltitudeBot.java @@ -11,22 +11,41 @@ import lombok.Getter; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.requests.GatewayIntent; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; import java.io.File; +import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Path; +@SpringBootApplication public class AltitudeBot { private JDA jda; @Getter private static AltitudeBot instance; + private static String path; + public static void main(String[] args) { + if (args.length == 0) { //TODO change scripts so it works with this + System.out.println("Please give the location for the configs as an arg"); + return; + } + path = args[0]; instance = new AltitudeBot(); - instance.start(); + instance.start(args); +// SpringApplication.run(AltitudeBot.class, args); + new SpringApplicationBuilder(AltitudeBot.class) + .web(WebApplicationType.SERVLET) // .REACTIVE, .SERVLET + .run(args); } - private void start() { + private void start(String[] args) { +// Logger.altitudeLogs.info("Starting spring application"); +// SpringApplication.run(AltitudeBot.class, args); Logger.altitudeLogs.info("Starting bot..."); initConfigs(); jda = JDABuilder.createDefault(SettingsConfig.TOKEN, @@ -50,11 +69,11 @@ public class AltitudeBot { // } catch (IllegalArgumentException e) { // Logger.exception(e); // } - initListeners(); + initListeners(args); } - private void initListeners() { - jda.addEventListener(new JDAListener(jda)); + private void initListeners(String[] args) { + jda.addEventListener(new JDAListener(jda, args)); } private void initConfigs() { @@ -65,7 +84,13 @@ public class AltitudeBot { public String getDataFolder() { try { - return new File(AltitudeBot.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getPath(); +// return new File(AltitudeBot.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getPath(); + File file = Path.of(new URI("file://"+ path)).toFile(); + if (!file.exists() || !file.canWrite() || !file.isDirectory()) { + System.out.println("Directory does not exist or can't be written to: " + file.getPath()); + return null; + } + return file.getPath(); } catch (URISyntaxException e) { Logger.altitudeLogs.error("Unable to retrieve config directory"); Logger.altitudeLogs.error(e); diff --git a/src/main/java/com/alttd/buttonManager/ButtonManager.java b/src/main/java/com/alttd/buttonManager/ButtonManager.java index 3ee151f..caf555e 100644 --- a/src/main/java/com/alttd/buttonManager/ButtonManager.java +++ b/src/main/java/com/alttd/buttonManager/ButtonManager.java @@ -3,20 +3,17 @@ package com.alttd.buttonManager; import com.alttd.buttonManager.buttons.autoReminder.ButtonAccepted; import com.alttd.buttonManager.buttons.autoReminder.ButtonInProgress; import com.alttd.buttonManager.buttons.autoReminder.ButtonRejected; +import com.alttd.buttonManager.buttons.eventButton.EventButton; import com.alttd.buttonManager.buttons.remindMeConfirm.ButtonRemindMeCancel; import com.alttd.buttonManager.buttons.remindMeConfirm.ButtonRemindMeConfirm; import com.alttd.buttonManager.buttons.suggestionReview.ButtonSuggestionReviewAccept; import com.alttd.buttonManager.buttons.suggestionReview.ButtonSuggestionReviewDeny; -import com.alttd.util.Util; -import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.interactions.components.buttons.Button; -import net.dv8tion.jda.api.requests.RestAction; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.awt.*; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -34,6 +31,7 @@ public class ButtonManager extends ListenerAdapter { buttons.add(new ButtonAccepted()); buttons.add(new ButtonInProgress()); buttons.add(new ButtonRejected()); + buttons.add(new EventButton()); } public void addButton(DiscordButton button) { diff --git a/src/main/java/com/alttd/buttonManager/buttons/eventButton/EventButton.java b/src/main/java/com/alttd/buttonManager/buttons/eventButton/EventButton.java new file mode 100644 index 0000000..981102f --- /dev/null +++ b/src/main/java/com/alttd/buttonManager/buttons/eventButton/EventButton.java @@ -0,0 +1,101 @@ +package com.alttd.buttonManager.buttons.eventButton; + +import com.alttd.AltitudeBot; +import com.alttd.buttonManager.DiscordButton; +import com.alttd.database.queries.events.Event; +import com.alttd.util.Logger; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +public class EventButton extends DiscordButton { + + public EventButton() { + } + + @Override + public String getButtonId() { + return "event_button"; + } + + @Override + public void execute(ButtonInteractionEvent event) { + Event eventForButton = Event.getEvent(event.getMessageIdLong()); + if (eventForButton == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Event not found")).setEphemeral(true).queue(); + return; + } + + Member member = event.getMember(); + if (member == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This button can only be used within a guild")).setEphemeral(true).queue(); + return; + } + + if (Instant.now().isAfter(eventForButton.getStartTime())) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to process you joining" + eventForButton.getTitle() + ", the event might have ended!")).setEphemeral(true).queue(); + return; + } + + Optional any = member.getRoles().stream().filter(role -> role.getIdLong() == eventForButton.getRoleId()).findAny(); + if (any.isPresent()) { + updateJoinedEventUsersInEmbed(eventForButton); + event.replyEmbeds(Util.genericSuccessEmbed("Success", "Removed you from " + eventForButton.getTitle() + "!")).setEphemeral(true).queue(); + try { + eventForButton.getRole().getGuild().removeRoleFromMember(member, eventForButton.getRole()).queue(); + } catch (Exception e) { + Logger.altitudeLogs.error(e); + } + return; + } + + try { + eventForButton.getRole().getGuild().addRoleToMember(member, eventForButton.getRole()).queue(); + } catch (Exception e) { + Logger.altitudeLogs.error(e); + } + if (updateJoinedEventUsersInEmbed(eventForButton)) { + event.replyEmbeds(Util.genericSuccessEmbed("Success", "You joined " + eventForButton.getTitle() + "!")).setEphemeral(true).queue(); + } else { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Failed to add you to the event, contact an admin for help if needed")).setEphemeral(true).queue(); + } + } + + @Override + public Button getButton() { + return Button.primary(getButtonId(), "Join Event"); + } + + private boolean updateJoinedEventUsersInEmbed(Event event) { + Optional optionalMessage = event.getMessage(); + if (optionalMessage.isEmpty()) { + Logger.altitudeLogs.error("Unable to find event message"); + return false; + } + Message message = optionalMessage.get(); + List embeds = message.getEmbeds(); + if (embeds.isEmpty()) { + Logger.altitudeLogs.error("Unable to find event embed"); + return false; + } + + EmbedBuilder builder = new EmbedBuilder(embeds.get(0)); + builder.clearFields() + .addField("Event Start", "", true); + Guild guildById = AltitudeBot.getInstance().getJDA().getGuildById(event.getGuildId()); + if (guildById != null) { + List membersWithRole = guildById.getMembersWithRoles(event.getRole()); + builder.addField("Participants", String.valueOf(membersWithRole.size()), true); + } + MessageEmbed updatedEmbed = builder.build(); + message.editMessageEmbeds(updatedEmbed).queue(); + return true; + } + +} diff --git a/src/main/java/com/alttd/commandManager/commands/CommandAuction.java b/src/main/java/com/alttd/commandManager/commands/CommandAuction.java index 99784d6..db2cb73 100644 --- a/src/main/java/com/alttd/commandManager/commands/CommandAuction.java +++ b/src/main/java/com/alttd/commandManager/commands/CommandAuction.java @@ -142,8 +142,7 @@ public class CommandAuction extends DiscordCommand { private void addScreenshot(Message.Attachment screenshot, Message message) { String dataFolder = AltitudeBot.getInstance().getDataFolder(); - Path parent = Path.of(dataFolder).getParent(); - Path path = Path.of(parent.toString() + UUID.randomUUID() + "." + screenshot.getFileExtension()); + Path path = Path.of(dataFolder + UUID.randomUUID() + "." + screenshot.getFileExtension()); screenshot.getProxy().downloadToFile(path.toFile()).whenComplete((file, throwable) -> message.editMessageAttachments(AttachedFile.fromData(file)).queue(done -> file.delete(), failed -> { Util.handleFailure(failed); diff --git a/src/main/java/com/alttd/communication/contact/ContactEndpoint.java b/src/main/java/com/alttd/communication/contact/ContactEndpoint.java new file mode 100644 index 0000000..d9c45fc --- /dev/null +++ b/src/main/java/com/alttd/communication/contact/ContactEndpoint.java @@ -0,0 +1,52 @@ +package com.alttd.communication.contact; + +import com.alttd.AltitudeBot; +import com.alttd.communication.formData.ContactFormData; +import jakarta.validation.Valid; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.concurrent.CompletableFuture; + +@CrossOrigin(origins = "*") +@RestController +@RequestMapping("/api/contact") +public class ContactEndpoint { + + private static final Logger logger = LoggerFactory.getLogger(ContactEndpoint.class); + + + @PostMapping("/submitContactForm") + public CompletableFuture> sendFormToDiscord(@Valid @RequestBody ContactFormData formData) { + logger.debug("Sending form to Discord: " + formData); + MessageEmbed messageEmbed = formData.toMessageEmbed(); + Guild guild = AltitudeBot.getInstance().getJDA().getGuildById(514920774923059209L); + if (guild == null) { + logger.error("Unable to retrieve staff guild"); + return CompletableFuture.completedFuture(ResponseEntity.internalServerError().body("Failed to submit form to Discord")); + } + TextChannel channel = guild.getChannelById(TextChannel.class, 514922567883292673L); + if (channel == null) { + logger.error("Unable to retrieve contact form channel"); + return CompletableFuture.completedFuture(ResponseEntity.internalServerError().body("Failed to submit form to Discord")); + } + + return CompletableFuture.supplyAsync(() -> { + try { + Message complete = channel.sendMessageEmbeds(messageEmbed).complete(); + if (complete != null) + return ResponseEntity.ok(""); + } catch (Exception exception) { + logger.error("Failed to send message to Discord", exception); + } + return ResponseEntity.internalServerError().body("Failed to submit form to Discord"); + }); + } + +} diff --git a/src/main/java/com/alttd/communication/formData/ContactFormData.java b/src/main/java/com/alttd/communication/formData/ContactFormData.java new file mode 100644 index 0000000..016d7bc --- /dev/null +++ b/src/main/java/com/alttd/communication/formData/ContactFormData.java @@ -0,0 +1,51 @@ +package com.alttd.communication.formData; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import org.hibernate.validator.constraints.Length; + +import java.awt.*; + +public class ContactFormData extends Form { + + public ContactFormData(String username, String email, String question) { + this.username = username; + this.email = email; + this.question = question; + } + + @NotEmpty(message = "You have to provide a username") + @Length(min = 3, max = 16, message = "Usernames have to be between 3 and 16 characters") + @Pattern(regexp = "[a-zA-Z-0-9_]{3,16}", message = "Your username has to be a valid Minecraft username") + public final String username; + + @NotEmpty(message = "You have to provide an e-mail address") + @Email(regexp = "(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)])", + message = "This is not a valid e-mail address") + public final String email; + + @Length(min = 11, max = 2000, message = "Your question should have between 10 and 2000 characters") + public final String question; + + @Override + public String toString() { + return "ContactFormData{" + + "username='" + username + '\'' + + ", email='" + email + '\'' + + ", question='" + question + '\'' + + '}'; + } + + @Override + public MessageEmbed toMessageEmbed() { + return new EmbedBuilder() + .addField("Username", username, false) + .addField("email", email, false) + .appendDescription(question) + .setColor(Color.GREEN) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/alttd/communication/formData/Form.java b/src/main/java/com/alttd/communication/formData/Form.java new file mode 100644 index 0000000..8089cbf --- /dev/null +++ b/src/main/java/com/alttd/communication/formData/Form.java @@ -0,0 +1,11 @@ +package com.alttd.communication.formData; + +import net.dv8tion.jda.api.entities.MessageEmbed; + +public abstract class Form { + + @Override + public abstract String toString(); + + public abstract MessageEmbed toMessageEmbed(); +} diff --git a/src/main/java/com/alttd/config/AbstractConfig.java b/src/main/java/com/alttd/config/AbstractConfig.java index 62e4011..004ec66 100644 --- a/src/main/java/com/alttd/config/AbstractConfig.java +++ b/src/main/java/com/alttd/config/AbstractConfig.java @@ -27,7 +27,7 @@ public abstract class AbstractConfig { private ConfigurationNode config; protected AbstractConfig(String filename) { - init(new File(new File(AltitudeBot.getInstance().getDataFolder()).getParentFile(), filename), filename); + init(new File(new File(AltitudeBot.getInstance().getDataFolder()), filename), filename); } private void init(File file, String filename) { diff --git a/src/main/java/com/alttd/contextMenuManager/ContextMenuManager.java b/src/main/java/com/alttd/contextMenuManager/ContextMenuManager.java index 2d153ca..e6bf451 100644 --- a/src/main/java/com/alttd/contextMenuManager/ContextMenuManager.java +++ b/src/main/java/com/alttd/contextMenuManager/ContextMenuManager.java @@ -1,5 +1,6 @@ package com.alttd.contextMenuManager; +import com.alttd.contextMenuManager.contextMenus.ContextMenuCreateEvent; import com.alttd.contextMenuManager.contextMenus.ContextMenuForwardToKanboard; import com.alttd.contextMenuManager.contextMenus.ContextMenuRespondSuggestion; import com.alttd.modalManager.ModalManager; @@ -23,6 +24,7 @@ public class ContextMenuManager extends ListenerAdapter { public ContextMenuManager(ModalManager modalManager) { contextMenus = List.of( new ContextMenuRespondSuggestion(modalManager), + new ContextMenuCreateEvent(modalManager), new ContextMenuForwardToKanboard() ); } diff --git a/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuCreateEvent.java b/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuCreateEvent.java new file mode 100644 index 0000000..90fc379 --- /dev/null +++ b/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuCreateEvent.java @@ -0,0 +1,68 @@ +package com.alttd.contextMenuManager.contextMenus; + +import com.alttd.contextMenuManager.DiscordContextMenu; +import com.alttd.modalManager.ModalManager; +import com.alttd.modalManager.modals.ModalCreateEvent; +import com.alttd.util.Util; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.UserContextInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.requests.RestAction; + +public class ContextMenuCreateEvent extends DiscordContextMenu { + + private final ModalManager modalManager; + + public ContextMenuCreateEvent(ModalManager modalManager) { + this.modalManager = modalManager; + } + + @Override + public String getContextMenuId() { + return "Create Event"; + } + + @Override + public void execute(UserContextInteractionEvent event) { + event.getInteraction().replyEmbeds(Util.genericErrorEmbed("Error", "This interaction should have been a message interaction")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } + + @Override + public void execute(MessageContextInteractionEvent event) { + if (!event.isFromGuild()) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This has to be done in a guild")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + Message message = event.getInteraction().getTarget(); + + if (message.getChannel().getIdLong() != 1172922338023591956L || message.getAuthor().getIdLong() != event.getUser().getIdLong()) { + event.getInteraction().replyEmbeds(Util.genericErrorEmbed("Error", "You can only use this on your own community post")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + Modal createEvent = modalManager.getModalFor("create_event"); + if (createEvent == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find create event modal")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + ModalCreateEvent.userToMessageTracker.putMessage(event.getUser().getIdLong(), message); //TODO find a better way to do this + event.replyModal(createEvent).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + } + + @Override + public CommandData getUserContextInteraction() { + return Commands.message(getContextMenuId()) + .setGuildOnly(true) + .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.MESSAGE_SEND)); + } +} diff --git a/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuRespondSuggestion.java b/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuRespondSuggestion.java index 24cfe7d..0a3ac92 100644 --- a/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuRespondSuggestion.java +++ b/src/main/java/com/alttd/contextMenuManager/contextMenus/ContextMenuRespondSuggestion.java @@ -56,7 +56,7 @@ public class ContextMenuRespondSuggestion extends DiscordContextMenu { return; } - ModalReplySuggestion.putMessage(event.getUser().getIdLong(), message); //TODO find a better way to do this + ModalReplySuggestion.userToMessageTracker.putMessage(event.getUser().getIdLong(), message); //TODO find a better way to do this event.replyModal(replySuggestion).queue(RestAction.getDefaultSuccess(), Util::handleFailure); } diff --git a/src/main/java/com/alttd/database/DatabaseTables.java b/src/main/java/com/alttd/database/DatabaseTables.java index 167bbce..ff19fc6 100644 --- a/src/main/java/com/alttd/database/DatabaseTables.java +++ b/src/main/java/com/alttd/database/DatabaseTables.java @@ -176,6 +176,39 @@ public class DatabaseTables { } } + private void createSettingsTable() { + String sql = "CREATE TABLE IF NOT EXISTS settings(" + + "name VARCHAR(32) NOT NULL, " + + "value VARCHAR(64) NOT NULL, " + + "type VARCHAR(16) NOT NULL, " + + "PRIMARY KEY (name)" + + ")"; + try { + connection.prepareStatement(sql).executeUpdate(); + } catch (SQLException e) { + Logger.altitudeLogs.error(e); + Logger.altitudeLogs.error("Unable to create auction settings table, shutting down..."); + } + } + + private void createEventsTable() { + String sql = "CREATE TABLE IF NOT EXISTS events(" + + "message_id BIGINT NOT NULL, " + + "channel_id BIGINT NOT NULL, " + + "guild_id BIGINT NOT NULL, " + + "epoch_second BIGINT NOT NULL, " + + "role_id BIGINT NOT NULL," + + "title VARCHAR(128) NOT NULL, " + + "PRIMARY KEY (message_id)" + + ")"; + try { + connection.prepareStatement(sql).executeUpdate(); + } catch (SQLException e) { + Logger.altitudeLogs.error(e); + Logger.altitudeLogs.error("Unable to create auction settings table, shutting down..."); + } + } + public static void createTables(Connection connection) { if (instance == null) instance = new DatabaseTables(connection); diff --git a/src/main/java/com/alttd/database/queries/events/Event.java b/src/main/java/com/alttd/database/queries/events/Event.java new file mode 100644 index 0000000..a0582ad --- /dev/null +++ b/src/main/java/com/alttd/database/queries/events/Event.java @@ -0,0 +1,79 @@ +package com.alttd.database.queries.events; + +import com.alttd.AltitudeBot; +import com.alttd.util.Logger; +import lombok.Getter; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; + +import java.time.Instant; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Optional; + + +public final class Event { + + private static final HashMap eventMap = new HashMap<>(); + @Getter + private final long messageId, guildId, channelId, roleId; + @Getter + private final Instant startTime; + @Getter + private Role role = null; + @Getter + private final String title; + + + public static Event getEvent(long messageId) { + return eventMap.get(messageId); + } + + private Message message = null; + + public Event(long messageId, long guildId, long channelId, Instant startTime, long roleId, String title) { + this.messageId = messageId; + this.guildId = guildId; + this.channelId = channelId; + this.startTime = startTime; + this.roleId = roleId; + this.title = title; + + eventMap.put(messageId, this); + loadMessageAndRole(); + Logger.altitudeLogs.info(String.format("Loaded event with title [%s] and message id [%s]", title, messageId)); + } + + public static Optional getNextEvent() { + return eventMap.values().stream().min(Comparator.comparing(Event::getStartTime)); + } + + public static void removeEvent(Event event) { + eventMap.remove(event.getMessageId()); + } + + private void loadMessageAndRole() { + Guild guild = AltitudeBot.getInstance().getJDA().getGuildById(guildId); + if (guild == null) { + Logger.altitudeLogs.error(String.format("Unable to find guild %s when creating event", guildId)); + return; + } + TextChannel textChannel = guild.getTextChannelById(channelId); + if (textChannel == null) { + Logger.altitudeLogs.error(String.format("Unable to find text channel %s when creating event", channelId)); + return; + } + textChannel.retrieveMessageById(messageId).queue(message -> { + this.message = message; + Logger.altitudeLogs.debug(String.format("Loaded message for event with title [%s]", title)); + }); + role = guild.getRoleById(roleId); + } + + public Optional getMessage() { + return message == null ? Optional.empty() : Optional.of(message); + } + +}; diff --git a/src/main/java/com/alttd/database/queries/events/QueriesEvent.java b/src/main/java/com/alttd/database/queries/events/QueriesEvent.java new file mode 100644 index 0000000..38fe0d7 --- /dev/null +++ b/src/main/java/com/alttd/database/queries/events/QueriesEvent.java @@ -0,0 +1,59 @@ +package com.alttd.database.queries.events; + +import com.alttd.database.Database; +import com.alttd.util.Logger; +import net.dv8tion.jda.api.entities.Message; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; + +public class QueriesEvent { + + public void addEvent(Message message, Instant start, long roleId, String title) { + String sql = "INSERT INTO events VALUES(?, ?, ?, ?, ?, ?)"; + + try { + PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql); + + preparedStatement.setLong(1, message.getIdLong()); + preparedStatement.setLong(2, message.getChannelIdLong()); + preparedStatement.setLong(3, message.getGuildIdLong()); + preparedStatement.setLong(4, start.getEpochSecond()); + preparedStatement.setLong(5, roleId); + preparedStatement.setString(6, title); + + preparedStatement.executeUpdate(); + } catch (SQLException exception) { + Logger.altitudeLogs.error(exception); + } + } + + public void loadActiveEvents() { + String sql = "SELECT * FROM events WHERE epoch_second > ?"; + int counter = 0; + try { + PreparedStatement preparedStatement = Database.getDatabase().getConnection().prepareStatement(sql); + + preparedStatement.setLong(1, Instant.now().getEpochSecond()); + + ResultSet resultSet = preparedStatement.executeQuery(); + while (resultSet.next()) { + new Event( + resultSet.getLong("message_id"), + resultSet.getLong("guild_id"), + resultSet.getLong("channel_id"), + Instant.ofEpochSecond(resultSet.getLong("epoch_second")), + resultSet.getLong("role_id"), + resultSet.getString("title") + ); + counter++; + } + } catch (SQLException exception) { + Logger.altitudeLogs.error(exception); + } + Logger.altitudeLogs.info(String.format("Loaded %d events", counter)); + } + +} diff --git a/src/main/java/com/alttd/database/queries/settings/QueriesSettings.java b/src/main/java/com/alttd/database/queries/settings/QueriesSettings.java new file mode 100644 index 0000000..209e197 --- /dev/null +++ b/src/main/java/com/alttd/database/queries/settings/QueriesSettings.java @@ -0,0 +1,4 @@ +package com.alttd.database.queries.settings; + +public class QueriesSettings { +} diff --git a/src/main/java/com/alttd/database/queries/settings/Setting.java b/src/main/java/com/alttd/database/queries/settings/Setting.java new file mode 100644 index 0000000..f58d286 --- /dev/null +++ b/src/main/java/com/alttd/database/queries/settings/Setting.java @@ -0,0 +1,59 @@ +package com.alttd.database.queries.settings; + +import com.alttd.database.Database; + +import java.sql.*; +import java.util.List; +import java.util.stream.Collectors; + +public class Setting { + + private final List allowedClasses = List.of(Boolean.class, Integer.class, Long.class, Float.class, Double.class, String.class); + + public void insertSetting(String key, T value, String type) throws SQLException, IllegalArgumentException { + if (!allowedClasses.contains(type)) { + throw new IllegalArgumentException(String.format("Invalid type, the only allowed types are: %s", allowedClasses.stream().map(Class::getSimpleName).collect(Collectors.joining(", ")))); + } + String query = "INSERT INTO settings (name, value, type) VALUES (?, ?, ?)"; + + try (Connection connection = DriverManager.getConnection("your_connection_string"); + PreparedStatement preparedStatement = connection.prepareStatement(query)) { + + preparedStatement.setString(1, key); + preparedStatement.setString(2, value.toString()); + preparedStatement.setString(3, type); + preparedStatement.executeUpdate(); + } + } + + public T getSetting(String key, Class type) throws SQLException, IllegalArgumentException { + if (!allowedClasses.contains(type)) { + throw new IllegalArgumentException(String.format("Invalid type, the only allowed types are: %s", allowedClasses.stream().map(Class::getSimpleName).collect(Collectors.joining(", ")))); + } + String query = "SELECT value, type FROM settings WHERE name = ?"; + + try (Connection connection = Database.getDatabase().getConnection(); + PreparedStatement preparedStatement = connection.prepareStatement(query)) { + + preparedStatement.setString(1, key); + ResultSet resultSet = preparedStatement.executeQuery(); + if (resultSet.next()) { + String dbType = resultSet.getString("type"); + if (!dbType.equals(type.getSimpleName())) { + throw new IllegalArgumentException(String.format("%s is of type %s not %s", key, dbType, type.getSimpleName())); + } + String value = resultSet.getString("value"); + if (type.equals(Integer.class)) { + return type.cast(Integer.parseInt(value)); + } else if (type.equals(Boolean.class)) { + return type.cast(Boolean.parseBoolean(value)); + } // and so on for other types + else { + return type.cast(value); + } + } + } + + throw new SQLException("Key not found in settings"); + } +} \ No newline at end of file diff --git a/src/main/java/com/alttd/listeners/JDAListener.java b/src/main/java/com/alttd/listeners/JDAListener.java index ede699f..7f90577 100644 --- a/src/main/java/com/alttd/listeners/JDAListener.java +++ b/src/main/java/com/alttd/listeners/JDAListener.java @@ -1,11 +1,14 @@ package com.alttd.listeners; +import com.alttd.AltitudeBot; import com.alttd.buttonManager.ButtonManager; import com.alttd.commandManager.CommandManager; import com.alttd.contextMenuManager.ContextMenuManager; import com.alttd.database.queries.Poll.PollQueries; +import com.alttd.database.queries.events.QueriesEvent; import com.alttd.modalManager.ModalManager; import com.alttd.schedulers.AuctionScheduler; +import com.alttd.schedulers.EventTimerTask; import com.alttd.schedulers.PollTimerTask; import com.alttd.schedulers.ReminderScheduler; import com.alttd.request.RequestManager; @@ -19,6 +22,7 @@ import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.GenericSelectMenuInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.jetbrains.annotations.NotNull; +import org.springframework.boot.SpringApplication; import java.util.Timer; import java.util.concurrent.TimeUnit; @@ -26,9 +30,11 @@ import java.util.concurrent.TimeUnit; public class JDAListener extends ListenerAdapter { private final JDA jda; + private final String[] args; - public JDAListener(JDA jda) { + public JDAListener(JDA jda, String[] args) { this.jda = jda; + this.args = args; } @Override @@ -45,8 +51,12 @@ public class JDAListener extends ListenerAdapter { jda.addEventListener(buttonManager, tagAdded, modalManager, commandManager, contextMenuManager, lockedChannel, appealRepost, selectMenuManager); PollQueries.loadPolls(buttonManager); new Timer().scheduleAtFixedRate(new PollTimerTask(jda, Logger.altitudeLogs), TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(5)); + new QueriesEvent().loadActiveEvents(); + new Timer().scheduleAtFixedRate(new EventTimerTask(), TimeUnit.MINUTES.toMillis(1), TimeUnit.MINUTES.toMillis(1)); startSchedulers(); // RequestManager.init(); +// Logger.altitudeLogs.info("Starting spring application"); +// SpringApplication.run(AltitudeBot.class, args); } private void startSchedulers() { diff --git a/src/main/java/com/alttd/modalManager/ModalManager.java b/src/main/java/com/alttd/modalManager/ModalManager.java index 38ca225..619d4be 100644 --- a/src/main/java/com/alttd/modalManager/ModalManager.java +++ b/src/main/java/com/alttd/modalManager/ModalManager.java @@ -2,16 +2,12 @@ package com.alttd.modalManager; import com.alttd.buttonManager.ButtonManager; import com.alttd.modalManager.modals.*; -import com.alttd.util.Util; -import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.interactions.modals.Modal; -import net.dv8tion.jda.api.requests.RestAction; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.awt.*; import java.util.List; import java.util.Optional; @@ -25,7 +21,8 @@ public class ModalManager extends ListenerAdapter { new ModalEvidence(), new ModalReplySuggestion(), new ModalRemindMe(buttonManager), - new ModalCrateItem()); + new ModalCrateItem(), + new ModalCreateEvent(buttonManager)); } @Override diff --git a/src/main/java/com/alttd/modalManager/modals/ModalCrateItem.java b/src/main/java/com/alttd/modalManager/modals/ModalCrateItem.java index c39ed47..f479ec5 100644 --- a/src/main/java/com/alttd/modalManager/modals/ModalCrateItem.java +++ b/src/main/java/com/alttd/modalManager/modals/ModalCrateItem.java @@ -11,7 +11,6 @@ import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; -import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.modals.Modal; import net.dv8tion.jda.api.interactions.components.text.TextInput; import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; @@ -119,7 +118,11 @@ public class ModalCrateItem extends DiscordModal { .build(); return Modal.create(getModalId(), "Crate Item Suggestion") - .addActionRows(ActionRow.of(item), ActionRow.of(itemName), ActionRow.of(lore), ActionRow.of(enchants), ActionRow.of(explanation)) + .addActionRow(item) + .addActionRow(itemName) + .addActionRow(lore) + .addActionRow(enchants) + .addActionRow(explanation) .build(); } diff --git a/src/main/java/com/alttd/modalManager/modals/ModalCreateEvent.java b/src/main/java/com/alttd/modalManager/modals/ModalCreateEvent.java new file mode 100644 index 0000000..96c1766 --- /dev/null +++ b/src/main/java/com/alttd/modalManager/modals/ModalCreateEvent.java @@ -0,0 +1,150 @@ +package com.alttd.modalManager.modals; + +import com.alttd.buttonManager.ButtonManager; +import com.alttd.database.queries.events.Event; +import com.alttd.database.queries.events.QueriesEvent; +import com.alttd.modalManager.DiscordModal; +import com.alttd.util.UserToMessageTracker; +import com.alttd.util.Util; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import net.dv8tion.jda.api.interactions.modals.Modal; +import net.dv8tion.jda.api.interactions.modals.ModalMapping; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.requests.restaction.RoleAction; +import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction; +import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; +import net.dv8tion.jda.api.utils.messages.MessageCreateData; + +import java.awt.*; +import java.time.Instant; +import java.util.Random; +import java.util.stream.Collectors; + +public class ModalCreateEvent extends DiscordModal { + + public static final UserToMessageTracker userToMessageTracker = new UserToMessageTracker(); + private final ButtonManager buttonManager; + + public ModalCreateEvent(ButtonManager buttonManager) { + this.buttonManager = buttonManager; + } + + @Override + public String getModalId() { + return "create_event"; + } + + @Override + public void execute(ModalInteractionEvent event) { + ModalMapping titleModalMapping = event.getInteraction().getValue("title"); + ModalMapping timeModalMapping = event.getInteraction().getValue("time"); + if (titleModalMapping == null || timeModalMapping == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find time or title in modal")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + String title = titleModalMapping.getAsString(); + String time = timeModalMapping.getAsString(); + if (title.isEmpty() || time.isEmpty()) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Response in modal is empty")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + Instant eventStart; + try { + eventStart = Instant.ofEpochSecond(Long.parseLong(time)); + } catch (NumberFormatException e) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "Invalid time format, try again")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + Member member = event.getMember(); + if (member == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "This modal only works from within a guild")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + Message message = userToMessageTracker.pullMessage(member.getIdLong()); + if (message == null) { + event.replyEmbeds(Util.genericErrorEmbed("Error", "No message found to create an event from")) + .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); + return; + } + + MessageEmbed messageEmbed = new EmbedBuilder() + .setTitle(title) + .addField("Event Start", "", true) + .addField("Participants", "0", true) + .setFooter("Click the join button to be notified when the event starts") + .setColor(Color.GREEN) + .build(); + + Button eventButton = buttonManager.getButtonFor("event_button"); + + try (MessageCreateData build = new MessageCreateBuilder() + .setEmbeds(messageEmbed) + .setActionRow(eventButton) + .build()) { + + Guild guild = message.getGuild(); + ReplyCallbackAction replyCallbackAction = event.deferReply(true); + createRole(guild).queue(role -> { + message.reply(build).queue(newMessage -> { + new QueriesEvent().addEvent(newMessage, eventStart, role.getIdLong(), title); + new Event(newMessage.getIdLong(), newMessage.getGuildIdLong(), newMessage.getChannelIdLong(), eventStart, role.getIdLong(), title); + }); + replyCallbackAction.setEmbeds(Util.genericSuccessEmbed("Success", "Your event has been created")).queue(); + }, failed -> { + replyCallbackAction.setEmbeds(Util.genericErrorEmbed("Error", "Unable to create event")).queue(); + }); + } + } + + private RoleAction createRole(Guild guild) { + String possibleCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + Random rand = new Random(); + String randomStr = rand.ints(5, 0, possibleCharacters.length()) + .mapToObj(i -> possibleCharacters.charAt(i) + "") + .collect(Collectors.joining()); + + return guild.createRole().setName("event role " + randomStr) + .setMentionable(false) + .setHoisted(false); + } + + @Override + public Modal getModal() { + String currentTimestamp = String.valueOf(Instant.now().getEpochSecond()); + TextInput time = TextInput.create("time", "Epoch time, see https://epochconverter.com/", TextInputStyle.SHORT) + .setPlaceholder(currentTimestamp) + .setMinLength(currentTimestamp.length()) + .setMaxLength(currentTimestamp.length() + 1) + .setRequired(true) + .build(); + + TextInput title = TextInput.create("title", "Event title", TextInputStyle.SHORT) + .setPlaceholder("The title for your event") + .setMinLength(5) + .setMaxLength(128) + .setRequired(true) + .build(); + + return Modal.create(getModalId(), "Create an event") + .addComponents(ActionRow.of(title), ActionRow.of(time)) + .build(); + } +} diff --git a/src/main/java/com/alttd/modalManager/modals/ModalEvidence.java b/src/main/java/com/alttd/modalManager/modals/ModalEvidence.java index 327db45..ba86223 100644 --- a/src/main/java/com/alttd/modalManager/modals/ModalEvidence.java +++ b/src/main/java/com/alttd/modalManager/modals/ModalEvidence.java @@ -11,7 +11,6 @@ import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; -import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.modals.Modal; import net.dv8tion.jda.api.interactions.components.text.TextInput; import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; @@ -111,7 +110,10 @@ public class ModalEvidence extends DiscordModal { .build(); return Modal.create(getModalId(), "Evidence") - .addActionRows(ActionRow.of(user), ActionRow.of(punishmentType), ActionRow.of(reason), ActionRow.of(evidence)) + .addActionRow(user) + .addActionRow(punishmentType) + .addActionRow(reason) + .addActionRow(evidence) .build(); } } diff --git a/src/main/java/com/alttd/modalManager/modals/ModalRemindMe.java b/src/main/java/com/alttd/modalManager/modals/ModalRemindMe.java index a1c8e44..4accb11 100644 --- a/src/main/java/com/alttd/modalManager/modals/ModalRemindMe.java +++ b/src/main/java/com/alttd/modalManager/modals/ModalRemindMe.java @@ -132,7 +132,8 @@ public class ModalRemindMe extends DiscordModal { .build(); return Modal.create(getModalId(), "Remind Me") - .addActionRows(ActionRow.of(title), ActionRow.of(desc)) + .addActionRow(title) + .addActionRow(desc) .build(); } diff --git a/src/main/java/com/alttd/modalManager/modals/ModalReplySuggestion.java b/src/main/java/com/alttd/modalManager/modals/ModalReplySuggestion.java index 51a8f17..3b30042 100644 --- a/src/main/java/com/alttd/modalManager/modals/ModalReplySuggestion.java +++ b/src/main/java/com/alttd/modalManager/modals/ModalReplySuggestion.java @@ -1,30 +1,20 @@ package com.alttd.modalManager.modals; import com.alttd.modalManager.DiscordModal; +import com.alttd.util.UserToMessageTracker; import com.alttd.util.Util; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; -import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.modals.Modal; import net.dv8tion.jda.api.interactions.components.text.TextInput; import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; import net.dv8tion.jda.api.interactions.modals.ModalMapping; import net.dv8tion.jda.api.requests.RestAction; -import java.util.HashMap; - public class ModalReplySuggestion extends DiscordModal { - private static final HashMap userToMessageMap = new HashMap<>(); - - public static synchronized void putMessage(long userId, Message message) { - userToMessageMap.put(userId, message); - } - - private static synchronized Message pullMessage(long userId) { - return userToMessageMap.remove(userId); - } + public final static UserToMessageTracker userToMessageTracker = new UserToMessageTracker(); @Override public String getModalId() { @@ -54,7 +44,7 @@ public class ModalReplySuggestion extends DiscordModal { return; } - Message message = pullMessage(member.getIdLong()); + Message message = userToMessageTracker.pullMessage(member.getIdLong()); if (message == null) { event.replyEmbeds(Util.genericErrorEmbed("Error", "Unable to find a message for this modal")) .setEphemeral(true).queue(RestAction.getDefaultSuccess(), Util::handleFailure); @@ -84,7 +74,7 @@ public class ModalReplySuggestion extends DiscordModal { .build(); return Modal.create(getModalId(), "Suggestion Response") - .addActionRows(ActionRow.of(body)) + .addActionRow(body) .build(); } } diff --git a/src/main/java/com/alttd/modalManager/modals/ModalSuggestion.java b/src/main/java/com/alttd/modalManager/modals/ModalSuggestion.java index 7e5978a..5139eff 100644 --- a/src/main/java/com/alttd/modalManager/modals/ModalSuggestion.java +++ b/src/main/java/com/alttd/modalManager/modals/ModalSuggestion.java @@ -13,7 +13,6 @@ import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; -import net.dv8tion.jda.api.interactions.components.ActionRow; import net.dv8tion.jda.api.interactions.modals.Modal; import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.interactions.components.text.TextInput; @@ -120,7 +119,8 @@ public class ModalSuggestion extends DiscordModal { .build(); return Modal.create(getModalId(), "Suggestion Form") - .addActionRows(ActionRow.of(title), ActionRow.of(body)) + .addActionRow(title) + .addActionRow(body) .build(); } } diff --git a/src/main/java/com/alttd/schedulers/EventTimerTask.java b/src/main/java/com/alttd/schedulers/EventTimerTask.java new file mode 100644 index 0000000..920426e --- /dev/null +++ b/src/main/java/com/alttd/schedulers/EventTimerTask.java @@ -0,0 +1,45 @@ +package com.alttd.schedulers; + +import com.alttd.database.queries.events.Event; +import com.alttd.util.Logger; +import net.dv8tion.jda.api.entities.Message; + +import java.time.Instant; +import java.util.Optional; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; + +public class EventTimerTask extends TimerTask { + @Override + public void run() { + checkShouldStartEvent(); + } + + private void checkShouldStartEvent(){ + Optional nextEvent = Event.getNextEvent(); + if (nextEvent.isEmpty()) { + return; + } + Event event = nextEvent.get(); + if (event.getStartTime().isAfter(Instant.now())) { + return; + } + + Event.removeEvent(event); + performEventAction(event); + event.getRole().delete().queueAfter(1, TimeUnit.HOURS); + checkShouldStartEvent(); + } + + private void performEventAction(Event event) { + Optional optionalMessage = event.getMessage(); + if (optionalMessage.isEmpty()) { + Logger.altitudeLogs.error("Unable to find message for event"); + return; + } + + Message message = optionalMessage.get(); + + message.reply(String.format("%s [%s] is starting!", event.getRole().getAsMention(), event.getTitle())).queue(); + } +} diff --git a/src/main/java/com/alttd/util/Logger.java b/src/main/java/com/alttd/util/Logger.java index 7571fc7..5153973 100644 --- a/src/main/java/com/alttd/util/Logger.java +++ b/src/main/java/com/alttd/util/Logger.java @@ -13,7 +13,7 @@ public class Logger { Logger.altitudeLogs = new AltitudeLogs().setTimeFormat("[HH:mm:ss] "); try { Logger.altitudeLogs - .setLogPath(new File(AltitudeBot.getInstance().getDataFolder()).getParent() + File.separator + "logs") + .setLogPath(new File(AltitudeBot.getInstance().getDataFolder()) + File.separator + "logs") .setLogName("debug.log", LogLevel.DEBUG) .setLogName("info.log", LogLevel.INFO) .setLogName("warning.log", LogLevel.WARNING) diff --git a/src/main/java/com/alttd/util/UserToMessageTracker.java b/src/main/java/com/alttd/util/UserToMessageTracker.java new file mode 100644 index 0000000..bd1438e --- /dev/null +++ b/src/main/java/com/alttd/util/UserToMessageTracker.java @@ -0,0 +1,19 @@ +package com.alttd.util; + +import net.dv8tion.jda.api.entities.Message; + +import java.util.HashMap; + +public class UserToMessageTracker { + + private final HashMap userToMessageMap = new HashMap<>(); + + public synchronized void putMessage(long userId, Message message) { + userToMessageMap.put(userId, message); + } + + public synchronized Message pullMessage(long userId) { + return userToMessageMap.remove(userId); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..cafbf31 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,2 @@ +server: + port: 8001 \ No newline at end of file