commit b40dd8b9b02dc99e03872303c2b317d29286346f Author: Logan Fick Date: Sat May 25 16:06:57 2019 -0400 Made LogalBot open source. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d6b8f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,163 @@ +# Created by https://www.gitignore.io/api/git,java,redis,linux,gradle,eclipse,visualstudiocode +# Edit at https://www.gitignore.io/?templates=git,java,redis,linux,gradle,eclipse,visualstudiocode + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +### Eclipse Patch ### +# Eclipse Core +.project + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Annotation Processing +.apt_generated + +.sts4-cache/ + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Redis ### +# Ignore redis binary dump (dump.rdb) files + +*.rdb + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +### Gradle ### +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +# End of https://www.gitignore.io/api/git,java,redis,linux,gradle,eclipse,visualstudiocode \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b75df5f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "configurations": [ + { + "type": "java", + "name": "Default", + "request": "launch", + "mainClass": "dev.logal.logalbot.Main", + "projectName": "LogalBot", + "env": { + "TOKEN": "an_invalid_placeholder_token", + "REDIS_HOST": "localhost" + } + } + ] +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2bb9ad2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..cf4f1fe --- /dev/null +++ b/build.gradle @@ -0,0 +1,31 @@ +buildscript { + repositories { + jcenter() + } + + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:4.0.4' + } +} + +apply plugin: 'java' +apply plugin: 'com.github.johnrengelman.shadow' + +repositories { + jcenter() +} + +dependencies { + implementation 'org.slf4j:slf4j-simple:1.7.26' + + implementation 'net.dv8tion:JDA:3.8.3_463' + implementation 'com.sedmelluq:lavaplayer:1.3.17' + implementation 'redis.clients:jedis:3.0.1' + implementation 'com.vdurmont:emoji-java:4.0.0' +} + +jar { + manifest { + attributes 'Main-Class': 'dev.logal.logalbot.Main' + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..9ab0a83 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..92402a8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-bin.zip \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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 +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="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "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, switch paths to Windows format before running java +if $cygwin ; 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=$((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" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..10e38dd --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'LogalBot' \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/Main.java b/src/main/java/dev/logal/logalbot/Main.java new file mode 100644 index 0000000..2cca058 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/Main.java @@ -0,0 +1,141 @@ +package dev.logal.logalbot; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import javax.security.auth.login.LoginException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.logal.logalbot.commands.administration.*; +import dev.logal.logalbot.commands.audio.*; +import dev.logal.logalbot.commands.fun.*; +import dev.logal.logalbot.commands.general.*; +import dev.logal.logalbot.events.*; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.CommandManager; +import dev.logal.logalbot.utils.DataManager; +import net.dv8tion.jda.core.AccountType; +import net.dv8tion.jda.core.JDA; +import net.dv8tion.jda.core.JDABuilder; + +public final class Main { + private static final Logger logger = LoggerFactory.getLogger(Main.class); + + private static final String token = System.getenv("TOKEN"); + private static JDA jda; + + private Main() { + // Static access only. + } + + public static final void main(final String[] arguments) { + logger.info("Beginning setup of LogalBot..."); + + logger.info("Verifying connection to Redis..."); + try { + DataManager.verifyConnection(); + } catch (final Throwable exception) { + logger.error("An error occurred while attempting to verify the connection to Redis!", exception); + System.exit(1); + } + + logger.info("Running any needed schema migrations..."); + try { + DataManager.runMigrations(); + } catch (final Throwable exception) { + logger.error("An error occurred while attempting to migrate the database!", exception); + System.exit(1); + } + + logger.info("Attempting to log into Discord..."); + try { + final JDABuilder builder = new JDABuilder(AccountType.BOT); + builder.setAutoReconnect(true); + builder.setAudioEnabled(true); + builder.setToken(token); + builder.addEventListener(new GuildReady()); + jda = builder.build().awaitReady(); + } catch (final LoginException exception) { + logger.error("The token specified is not valid."); + System.exit(1); + } catch (final Throwable exception) { + logger.error("An error occurred while attempting to set up JDA!", exception); + System.exit(1); + } + logger.info("Successfully logged into Discord as bot user '" + jda.getSelfUser().getName() + "'."); + + logger.info("Initializing..."); + AudioUtil.initializePlayerManager(); + + jda.addEventListener(new GuildVoiceLeave()); + jda.addEventListener(new GuildVoiceMove()); + jda.addEventListener(new GuildMessageReactionAdd()); + + // General Commands + CommandManager.registerCommand("about", new About(), false); + CommandManager.registerCommand("help", new Help(), false); + + // Fun Commands + CommandManager.registerCommand("8ball", new EightBall(), false); + + // Audio Commands + CommandManager.registerCommand("forceskip", new ForceSkip(), true); + CommandManager.registerCommandAlias("fs", "forceskip"); + CommandManager.registerCommandAlias("fskip", "forceskip"); + CommandManager.registerCommand("lock", new Lock(), true); + CommandManager.registerCommandAlias("l", "lock"); + CommandManager.registerCommand("nowplaying", new NowPlaying(), false); + CommandManager.registerCommandAlias("np", "nowplaying"); + CommandManager.registerCommand("pause", new Pause(), true); + CommandManager.registerCommand("play", new Play(), false); + CommandManager.registerCommandAlias("p", "play"); + CommandManager.registerCommandAlias("pl", "play"); + CommandManager.registerCommandAlias("add", "play"); + CommandManager.registerCommandAlias("a", "play"); + CommandManager.registerCommand("queue", new Queue(), false); + CommandManager.registerCommandAlias("q", "queue"); + CommandManager.registerCommand("remove", new Remove(), true); + CommandManager.registerCommandAlias("r", "remove"); + CommandManager.registerCommandAlias("x", "remove"); + CommandManager.registerCommandAlias("rem", "remove"); + CommandManager.registerCommandAlias("rm", "remove"); + CommandManager.registerCommand("reset", new Reset(), true); + CommandManager.registerCommandAlias("rst", "reset"); + CommandManager.registerCommand("skip", new Skip(), false); + CommandManager.registerCommandAlias("s", "skip"); + CommandManager.registerCommand("volume", new Volume(), true); + CommandManager.registerCommandAlias("v", "volume"); + CommandManager.registerCommandAlias("vol", "volume"); + CommandManager.registerCommand("shuffle", new Shuffle(), true); + CommandManager.registerCommandAlias("shuf", "shuffle"); + CommandManager.registerCommandAlias("shuff", "shuffle"); + CommandManager.registerCommandAlias("shfl", "shuffle"); + + // Administration Commands + CommandManager.registerCommand("whitelist", new Whitelist(), true); + CommandManager.registerCommandAlias("wl", "whitelist"); + CommandManager.registerCommand("settings", new Settings(), true); + CommandManager.registerCommandAlias("set", "settings"); + CommandManager.registerCommandAlias("setting", "settings"); + CommandManager.registerCommandAlias("configure", "settings"); + CommandManager.registerCommandAlias("config", "settings"); + CommandManager.registerCommandAlias("conf", "settings"); + + logger.info("Everything seems to be ready! Enabling command listener..."); + jda.addEventListener(new GuildMessageReceived()); + logger.info("Initialization complete!"); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/audio/AudioPlayerSendHandler.java b/src/main/java/dev/logal/logalbot/audio/AudioPlayerSendHandler.java new file mode 100644 index 0000000..178454e --- /dev/null +++ b/src/main/java/dev/logal/logalbot/audio/AudioPlayerSendHandler.java @@ -0,0 +1,48 @@ +package dev.logal.logalbot.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; + +import net.dv8tion.jda.core.audio.AudioSendHandler; +import net.dv8tion.jda.core.utils.Checks; + +public final class AudioPlayerSendHandler implements AudioSendHandler { + private final AudioPlayer audioPlayer; + private AudioFrame lastFrame; + + public AudioPlayerSendHandler(final AudioPlayer audioPlayer) { + Checks.notNull(audioPlayer, "Audio Player"); + + this.audioPlayer = audioPlayer; + } + + @Override + public final boolean canProvide() { + lastFrame = audioPlayer.provide(); + return lastFrame != null; + } + + @Override + public final byte[] provide20MsAudio() { + return lastFrame.getData(); + } + + @Override + public final boolean isOpus() { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/audio/TrackLoadHandler.java b/src/main/java/dev/logal/logalbot/audio/TrackLoadHandler.java new file mode 100644 index 0000000..feaa56d --- /dev/null +++ b/src/main/java/dev/logal/logalbot/audio/TrackLoadHandler.java @@ -0,0 +1,201 @@ +package dev.logal.logalbot.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.PermissionManager; +import dev.logal.logalbot.utils.TrackUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; +import net.dv8tion.jda.core.utils.Checks; + +public final class TrackLoadHandler implements AudioLoadResultHandler { + private final Logger logger = LoggerFactory.getLogger(TrackLoadHandler.class); + + private final Member requester; + private final TextChannel channel; + + public TrackLoadHandler(final Member requester, final TextChannel channel) { + Checks.notNull(requester, "Requester"); + Checks.notNull(channel, "Channel"); + + this.requester = requester; + this.channel = channel; + } + + @Override + public final void trackLoaded(final AudioTrack track) { + Checks.notNull(track, "Track"); + + final CommandResponse response; + final TrackScheduler scheduler = AudioUtil.getTrackScheduler(this.channel.getGuild()); + + final AudioTrackInfo info = track.getInfo(); + if (info.isStream) { + response = new CommandResponse("no_entry_sign", + "Sorry " + this.requester.getAsMention() + ", but streams cannot be added to the queue.") + .setDeletionDelay(10, TimeUnit.SECONDS); + response.sendResponse(this.channel); + return; + } + + if ((info.length >= 60000 && info.length <= 900000) || PermissionManager.isWhitelisted(this.requester)) { + response = new CommandResponse("no_entry_sign", + "Sorry " + this.requester.getAsMention() + + ", but you can only add tracks between 1 and 15 minutes in length.").setDeletionDelay(10, + TimeUnit.SECONDS); + response.sendResponse(this.channel); + return; + } + + if (scheduler.isQueued(track)) { + response = new CommandResponse("no_entry_sign", + "Sorry " + this.requester.getAsMention() + ", but that track is already queued.") + .setDeletionDelay(10, TimeUnit.SECONDS); + response.sendResponse(this.channel); + return; + } + + scheduler.addToQueue(track, requester); + response = new CommandResponse("notes", + this.requester.getAsMention() + " added the following track to the queue:"); + response.attachEmbed(TrackUtil.generateTrackInfoEmbed(track)); + response.sendResponse(this.channel); + } + + @Override + public final void playlistLoaded(final AudioPlaylist playlist) { + Checks.notNull(playlist, "Playlist"); + + CommandResponse response; + final TrackScheduler scheduler = AudioUtil.getTrackScheduler(this.channel.getGuild()); + + final AudioTrack selectedTrack = playlist.getSelectedTrack(); + AudioTrack track = null; + if (!playlist.isSearchResult() && selectedTrack != null) { + track = selectedTrack; + } else if (playlist.isSearchResult()) { + track = playlist.getTracks().get(0); + } + + if (track != null) { + if (scheduler.isQueued(track)) { + response = new CommandResponse("no_entry_sign", + "Sorry " + this.requester.getAsMention() + ", but that track is already queued.") + .setDeletionDelay(10, TimeUnit.SECONDS); + response.sendResponse(this.channel); + return; + } + + final AudioTrackInfo info = track.getInfo(); + if (info.isStream) { + response = new CommandResponse("no_entry_sign", + "Sorry " + this.requester.getAsMention() + ", but streams cannot be added to the queue.") + .setDeletionDelay(10, TimeUnit.SECONDS); + response.sendResponse(this.channel); + return; + } + + if ((info.length >= 60000 && info.length <= 900000) || PermissionManager.isWhitelisted(this.requester)) { + scheduler.addToQueue(track, this.requester); + response = new CommandResponse("notes", + this.requester.getAsMention() + " added the following track to the queue:"); + response.attachEmbed(TrackUtil.generateTrackInfoEmbed(track)); + response.sendResponse(this.channel); + } else { + response = new CommandResponse("no_entry_sign", "Sorry " + this.requester.getAsMention() + + ", but you are not allowed to add tracks less than 1 minute or greater than 15 minutes in length.") + .setDeletionDelay(10, TimeUnit.SECONDS); + response.sendResponse(this.channel); + } + } else { + if (PermissionManager.isWhitelisted(this.requester)) { + final ArrayList addedTracks = new ArrayList<>(); + for (final AudioTrack playlistTrack : playlist.getTracks()) { + if (!scheduler.isQueueFull()) { + if (!scheduler.isQueued(playlistTrack) && !playlistTrack.getInfo().isStream) { + scheduler.addToQueue(playlistTrack, this.requester); + addedTracks.add(playlistTrack); + } + } else { + break; + } + } + + if (addedTracks.size() == 0) { + response = new CommandResponse("no_entry_sign", + "Sorry " + this.requester.getAsMention() + + ", but none of the tracks in that playlist could be added.").setDeletionDelay(10, + TimeUnit.SECONDS); + response.sendResponse(this.channel); + } + + response = new CommandResponse("notes", + this.requester.getAsMention() + " added the following tracks to the queue:"); + response.attachEmbed(TrackUtil.generateTrackListInfoEmbed(addedTracks, false)); + response.sendResponse(this.channel); + } else { + response = new CommandResponse("no_entry_sign", + "Sorry " + this.requester.getAsMention() + + ", but you are not allowed to add playlists to the queue.").setDeletionDelay(10, + TimeUnit.SECONDS); + response.sendResponse(this.channel); + } + } + } + + @Override + public final void noMatches() { + final CommandResponse response = new CommandResponse("map", + "Sorry " + this.requester.getAsMention() + ", but I was not able to find that track.") + .setDeletionDelay(10, TimeUnit.SECONDS); + response.sendResponse(this.channel); + } + + @Override + public final void loadFailed(final FriendlyException exception) { + Checks.notNull(exception, "Exception"); + + final CommandResponse response; + if (exception.getMessage().equals("Unknown file format.")) { + response = new CommandResponse("question", + "Sorry " + this.requester.getAsMention() + ", but I do not recognize the format of that track.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } else { + final Guild guild = this.channel.getGuild(); + logger.error("An error occurred for " + guild.getName() + " (" + guild.getId() + + ") while trying to load a track!", exception); + response = new CommandResponse("sos", + "Sorry " + this.requester.getAsMention() + + ", but an error occurred while trying to get that track!").setDeletionDelay(10, + TimeUnit.SECONDS); + } + response.sendResponse(this.channel); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/audio/TrackScheduler.java b/src/main/java/dev/logal/logalbot/audio/TrackScheduler.java new file mode 100644 index 0000000..e982814 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/audio/TrackScheduler.java @@ -0,0 +1,176 @@ +package dev.logal.logalbot.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.logal.logalbot.tasks.IdleDisconnectTask; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.DataManager; +import dev.logal.logalbot.utils.PermissionManager; +import dev.logal.logalbot.utils.Scheduler; +import dev.logal.logalbot.utils.SkipTracker; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.User; +import net.dv8tion.jda.core.entities.VoiceChannel; +import net.dv8tion.jda.core.utils.Checks; + +public final class TrackScheduler extends AudioEventAdapter { + private static final Logger logger = LoggerFactory.getLogger(TrackScheduler.class); + + private final Guild guild; + private final ArrayList queue = new ArrayList<>(250); + private boolean queueLocked = false; + private ScheduledFuture idleLogoutTask; + + public TrackScheduler(final Guild guild) { + Checks.notNull(guild, "Guild"); + + this.guild = guild; + } + + public final void addToQueue(final AudioTrack track, final Member requester) { + Checks.notNull(track, "Track"); + Checks.notNull(requester, "Requester"); + + if (this.queueLocked && !PermissionManager.isWhitelisted(requester)) { + return; + } + + if (this.isQueueFull()) { + return; + } + + final User user = requester.getUser(); + logger.info(user.getName() + " (" + user.getId() + ") added '" + track.getInfo().title + "' to the queue in " + + this.guild.getName() + " (" + this.guild.getId() + ")."); + this.queue.add(track); + if (!AudioUtil.isTrackLoaded(this.guild)) { + AudioUtil.openAudioConnection(VoiceChannelUtil.getVoiceChannelMemberIsConnectedTo(requester)); + AudioUtil.playTrack(this.guild, this.queue.remove(0)); + } + } + + public final void removeFromQueue(final int index) { + Checks.notNull(index, "Index"); + + logger.info("Track '" + queue.remove(index).getInfo().title + "' has been removed from the queue in " + + this.guild.getName() + " (" + this.guild.getId() + ")."); + } + + public final boolean isQueued(final AudioTrack track) { + Checks.notNull(track, "Track"); + + for (final AudioTrack queuedTrack : queue) { + if (track.getInfo().identifier.equals(queuedTrack.getInfo().identifier)) { + return true; + } + } + return false; + } + + public final boolean isQueueFull() { + return this.queue.size() >= 250; + } + + public final boolean isQueueEmpty() { + return this.queue.isEmpty(); + } + + public final boolean isQueueLocked() { + return this.queueLocked; + } + + public final void setQueueLocked(final boolean locked) { + Checks.notNull(locked, "Locked"); + + this.queueLocked = locked; + } + + public final void clearQueue() { + this.queue.clear(); + } + + public final void shuffleQueue() { + Collections.shuffle(this.queue); + } + + public final ArrayList getQueue() { + return this.queue; + } + + public final void skipCurrentTrack() { + if (AudioUtil.isTrackLoaded(this.guild)) { + logger.info("Track '" + AudioUtil.getLoadedTrack(this.guild).getInfo().title + "' in " + + this.guild.getName() + " (" + this.guild.getId() + ") been skipped."); + AudioUtil.stopTrack(this.guild); + } + } + + @Override + public final void onTrackStart(final AudioPlayer player, final AudioTrack track) { + Checks.notNull(player, "Player"); + Checks.notNull(track, "Track"); + + logger.info("Track '" + track.getInfo().title + "' in " + this.guild.getName() + " (" + this.guild.getId() + + ") has started."); + if (this.idleLogoutTask != null && !this.idleLogoutTask.isDone()) { + logger.info("A track has started in " + this.guild.getName() + " (" + this.guild.getId() + + "). Cancelling scheduled disconnect."); + this.idleLogoutTask.cancel(true); + } + SkipTracker.resetVotes(this.guild); + } + + @Override + public final void onTrackEnd(final AudioPlayer player, final AudioTrack track, + final AudioTrackEndReason endReason) { + Checks.notNull(player, "Player"); + Checks.notNull(track, "Track"); + Checks.notNull(endReason, "End reason"); + + logger.info("Track '" + track.getInfo().title + "' in " + this.guild.getName() + " (" + this.guild.getId() + + ") has stopped."); + if ((endReason.mayStartNext || endReason == AudioTrackEndReason.STOPPED) && this.queue.size() >= 1) { + AudioUtil.playTrack(this.guild, this.queue.remove(0)); + } else { + try { + AudioUtil.setVolume(this.guild, Integer.parseInt(DataManager.getGuildValue(guild, "defaultVolume"))); + } catch (final NumberFormatException exception) { + AudioUtil.setVolume(this.guild, 10); + } + this.queueLocked = false; + AudioUtil.setPausedState(this.guild, false); + final VoiceChannel currentChannel = AudioUtil.getVoiceChannelConnectedTo(this.guild); + logger.info("Disconnecting from " + currentChannel.getName() + " (" + currentChannel.getId() + ") in " + + this.guild.getName() + " (" + this.guild.getId() + ") in 1 minute..."); + this.idleLogoutTask = Scheduler.schedule(new IdleDisconnectTask(this.guild), 1, TimeUnit.MINUTES); + } + SkipTracker.resetVotes(this.guild); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/Command.java b/src/main/java/dev/logal/logalbot/commands/Command.java new file mode 100644 index 0000000..1688228 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/Command.java @@ -0,0 +1,22 @@ +package dev.logal.logalbot.commands; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public interface Command { + CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel); +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/CommandResponse.java b/src/main/java/dev/logal/logalbot/commands/CommandResponse.java new file mode 100644 index 0000000..e1573be --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/CommandResponse.java @@ -0,0 +1,92 @@ +package dev.logal.logalbot.commands; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import com.vdurmont.emoji.Emoji; +import com.vdurmont.emoji.EmojiManager; + +import dev.logal.logalbot.tasks.MessageDeleteTask; +import dev.logal.logalbot.utils.ReactionCallbackManager; +import dev.logal.logalbot.utils.Scheduler; +import net.dv8tion.jda.core.MessageBuilder; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.Message; +import net.dv8tion.jda.core.entities.MessageEmbed; +import net.dv8tion.jda.core.entities.TextChannel; +import net.dv8tion.jda.core.entities.User; + +public final class CommandResponse { + private final Emoji emoji; + private final String response; + private final LinkedHashMap callbacks = new LinkedHashMap<>(); + private MessageEmbed responseEmbed; + private User callbacksTarget; + + private long deletionDelay = 0; + private TimeUnit deletionDelayUnit; + + public CommandResponse(final String emoji, final String response) { + this.emoji = EmojiManager.getForAlias(emoji); + this.response = response; + } + + public final CommandResponse attachEmbed(final MessageEmbed embed) { + this.responseEmbed = embed; + return this; + } + + public final CommandResponse setDeletionDelay(final long delay, final TimeUnit unit) { + this.deletionDelay = delay; + this.deletionDelayUnit = unit; + return this; + } + + public final CommandResponse addReactionCallback(final Emoji emoji, final ReactionCallback callback) { + this.callbacks.put(emoji, callback); + return this; + } + + public final CommandResponse setReactionCallbackTarget(final Member member) { + this.callbacksTarget = member.getUser(); + return this; + } + + public final void sendResponse(final TextChannel channel) { + final MessageBuilder builder = new MessageBuilder(); + builder.setContent(this.emoji.getUnicode() + " " + this.response); + + if (this.responseEmbed != null) { + builder.setEmbed(this.responseEmbed); + } + + channel.sendMessage(builder.build()).queue(this::handleResponseCreation); + } + + private final void handleResponseCreation(final Message message) { + if ((this.deletionDelay != 0) && (this.deletionDelayUnit != null)) { + Scheduler.schedule(new MessageDeleteTask(message), this.deletionDelay, this.deletionDelayUnit); + } + + for (final Map.Entry callback : callbacks.entrySet()) { + ReactionCallbackManager.registerCallback(message, callback.getKey(), callback.getValue()); + ReactionCallbackManager.setCallbackTarget(this.callbacksTarget, message); + message.addReaction(callback.getKey().getUnicode()).queue(); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/ReactionCallback.java b/src/main/java/dev/logal/logalbot/commands/ReactionCallback.java new file mode 100644 index 0000000..b64356e --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/ReactionCallback.java @@ -0,0 +1,21 @@ +package dev.logal.logalbot.commands; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import net.dv8tion.jda.core.entities.Member; + +public interface ReactionCallback { + void run(final Member reactor, final long messageID); +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/administration/Settings.java b/src/main/java/dev/logal/logalbot/commands/administration/Settings.java new file mode 100644 index 0000000..98f2331 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/administration/Settings.java @@ -0,0 +1,123 @@ +package dev.logal.logalbot.commands.administration; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.DataManager; +import dev.logal.logalbot.utils.StringUtil; +import net.dv8tion.jda.core.EmbedBuilder; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Settings implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = executor.getGuild(); + + if (arguments.length == 0) { + final CommandResponse response = new CommandResponse("tools", + executor.getAsMention() + ", these are the current settings for this guild:"); + final EmbedBuilder builder = new EmbedBuilder(); + builder.setTitle("**Current Settings for " + StringUtil.sanitize(guild.getName()) + "**"); + + final String commandCharacter = DataManager.getGuildValue(guild, "commandCharacter"); + if (commandCharacter == null) { + builder.addField("Command Character", "Not Set", true); + } else { + builder.addField("Command Character", commandCharacter, true); + } + + final String defaultVolume = DataManager.getGuildValue(guild, "defaultVolume"); + if (defaultVolume == null) { + builder.addField("Default Volume", "10%", true); + } else { + builder.addField("Default Volume", defaultVolume + "%", true); + } + response.attachEmbed(builder.build()); + return response; + } + + if (arguments[0].equalsIgnoreCase("commandcharacter") || arguments[0].equalsIgnoreCase("cmdchar")) { + if (arguments.length == 1) { + DataManager.deleteGuildKey(guild, "commandCharacter"); + return new CommandResponse("white_check_mark", + executor.getAsMention() + ", the command character has been disabled."); + } else { + final char[] input = arguments[1].replaceAll("\n", "").toCharArray(); + if (input.length > 1) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but the command character must be a single character.").setDeletionDelay(10, + TimeUnit.SECONDS); + } else if (input.length == 0) { + DataManager.deleteGuildKey(guild, "commandCharacter"); + return new CommandResponse("white_check_mark", + executor.getAsMention() + ", the command character has been disabled."); + } else { + DataManager.setGuildValue(guild, "commandCharacter", "" + input[0]); + return new CommandResponse("white_check_mark", + executor.getAsMention() + ", the command character has been set to `" + input[0] + "`."); + } + } + } else if (arguments[0].equalsIgnoreCase("defaultvolume") || arguments[0].equalsIgnoreCase("volume")) { + if (arguments.length == 1) { + DataManager.deleteGuildKey(guild, "defaultVolume"); + if (!AudioUtil.isTrackLoaded(guild)) { + AudioUtil.setVolume(guild, 10); + } + return new CommandResponse("white_check_mark", + executor.getAsMention() + ", the default volume has been reset to `10%`."); + } else { + final int volume; + try { + volume = Integer.parseInt(arguments[1]); + } catch (final NumberFormatException exception) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but the default volume must be an integer.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (volume == 10) { + DataManager.deleteGuildKey(guild, "defaultVolume"); + return new CommandResponse("white_check_mark", + executor.getAsMention() + ", the default volume has been reset to `10%`."); + } + + if (volume <= 150 && volume >= 1) { + if (!AudioUtil.isTrackLoaded(guild)) { + AudioUtil.setVolume(guild, volume); + } + DataManager.setGuildValue(guild, "defaultVolume", "" + volume); + return new CommandResponse("white_check_mark", + executor.getAsMention() + ", the default volume has been set to `" + volume + "%`."); + } else { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but the default volume must be between 1% and 150%.").setDeletionDelay(10, + TimeUnit.SECONDS); + } + } + } else { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but I do not know what that setting is.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/administration/Whitelist.java b/src/main/java/dev/logal/logalbot/commands/administration/Whitelist.java new file mode 100644 index 0000000..f31ad8c --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/administration/Whitelist.java @@ -0,0 +1,82 @@ +package dev.logal.logalbot.commands.administration; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.PermissionManager; +import net.dv8tion.jda.core.Permission; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Whitelist implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (!executor.hasPermission(Permission.ADMINISTRATOR)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you are not allowed to use this command.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (arguments.length == 0) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but you need to specify a user to add or remove from the whitelist.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + final String userID = arguments[0].replaceFirst("<@[!]?([0-9]*)>", "$1"); + final Member member; + try { + member = guild.getMemberById(userID); + } catch (final Throwable exception) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but that doesn't appear to be a valid user.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (member == null) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but that doesn't appear to be a valid user.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (member.hasPermission(Permission.ADMINISTRATOR)) { + return new CommandResponse("no_entry_sign", "Sorry " + executor.getAsMention() + + ", but you cannot remove that user from the whitelist due to them being a guild administrator.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (member.getUser().isBot()) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you cannot whitelist bots.").setDeletionDelay(10, + TimeUnit.SECONDS); + } + + if (PermissionManager.isWhitelisted(member)) { + PermissionManager.removeFromWhitelist(member); + return new CommandResponse("heavy_multiplication_x", + executor.getAsMention() + " has removed " + member.getAsMention() + " from the whitelist."); + } else { + PermissionManager.addToWhitelist(member); + return new CommandResponse("heavy_check_mark", + executor.getAsMention() + " has added " + member.getAsMention() + " to the whitelist."); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/ForceSkip.java b/src/main/java/dev/logal/logalbot/commands/audio/ForceSkip.java new file mode 100644 index 0000000..d3eedaa --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/ForceSkip.java @@ -0,0 +1,56 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.TrackUtil; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class ForceSkip implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (!AudioUtil.isTrackLoaded(guild)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but there must be a track playing in order to force skip it.").setDeletionDelay(10, + TimeUnit.SECONDS); + } + + if (!VoiceChannelUtil.isInCurrentVoiceChannel(executor)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you must be in voice channel `" + + AudioUtil.getVoiceChannelConnectedTo(guild).getName() + + "` in order to force skip tracks.").setDeletionDelay(10, TimeUnit.SECONDS); + } + + final AudioTrack skippedTrack = AudioUtil.getLoadedTrack(guild); + + AudioUtil.getTrackScheduler(guild).skipCurrentTrack(); + final CommandResponse response = new CommandResponse("gun", + executor.getAsMention() + " force skipped the following track:"); + response.attachEmbed(TrackUtil.generateTrackInfoEmbed(skippedTrack)); + return response; + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/Lock.java b/src/main/java/dev/logal/logalbot/commands/audio/Lock.java new file mode 100644 index 0000000..0c1bbbe --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/Lock.java @@ -0,0 +1,55 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import dev.logal.logalbot.audio.TrackScheduler; +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Lock implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (!AudioUtil.isTrackLoaded(guild)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but there must be a track playing in order to lock or unlock the queue.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (!VoiceChannelUtil.isInCurrentVoiceChannel(executor)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you must be in voice channel `" + + AudioUtil.getVoiceChannelConnectedTo(guild).getName() + + "` in order to lock or unlock the queue.").setDeletionDelay(10, TimeUnit.SECONDS); + } + + final TrackScheduler scheduler = AudioUtil.getTrackScheduler(guild); + if (scheduler.isQueueLocked()) { + scheduler.setQueueLocked(false); + return new CommandResponse("unlock", executor.getAsMention() + " unlocked the queue."); + } else { + scheduler.setQueueLocked(true); + return new CommandResponse("lock", executor.getAsMention() + " locked the queue."); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/NowPlaying.java b/src/main/java/dev/logal/logalbot/commands/audio/NowPlaying.java new file mode 100644 index 0000000..2592328 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/NowPlaying.java @@ -0,0 +1,38 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.TrackUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class NowPlaying implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (!AudioUtil.isTrackLoaded(guild)) { + return new CommandResponse("mute", executor.getAsMention() + ", there is nothing currently playing."); + } + + final CommandResponse response = new CommandResponse("dancer", + executor.getAsMention() + ", this is the track currently playing:"); + response.attachEmbed(TrackUtil.generateCurrentTrackInfoEmbed(guild)); + return response; + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/Pause.java b/src/main/java/dev/logal/logalbot/commands/audio/Pause.java new file mode 100644 index 0000000..ed3aa1c --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/Pause.java @@ -0,0 +1,53 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Pause implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (!AudioUtil.isTrackLoaded(guild)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but there must be a track playing in order to pause or resume the track player.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (!VoiceChannelUtil.isInCurrentVoiceChannel(executor)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you must be in voice channel `" + + AudioUtil.getVoiceChannelConnectedTo(guild).getName() + + "` in order to pause or resume the track player.").setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (AudioUtil.isPlayerPaused(guild)) { + AudioUtil.setPausedState(guild, false); + return new CommandResponse("arrow_forward", executor.getAsMention() + " resumed the track player."); + } else { + AudioUtil.setPausedState(guild, true); + return new CommandResponse("pause_button", executor.getAsMention() + " paused the track player."); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/Play.java b/src/main/java/dev/logal/logalbot/commands/audio/Play.java new file mode 100644 index 0000000..3ba0339 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/Play.java @@ -0,0 +1,100 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import dev.logal.logalbot.audio.TrackScheduler; +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.PermissionManager; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.Permission; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; +import net.dv8tion.jda.core.entities.VoiceChannel; + +public final class Play implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (AudioUtil.isTrackLoaded(guild) && !VoiceChannelUtil.isInCurrentVoiceChannel(executor)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you need to be in voice channel `" + + AudioUtil.getVoiceChannelConnectedTo(guild).getName() + + "` in order to add songs to the queue.").setDeletionDelay(10, TimeUnit.SECONDS); + } + + final VoiceChannel targetChannel = VoiceChannelUtil.getVoiceChannelMemberIsConnectedTo(executor); + if (targetChannel == null) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but you need to be in a voice channel in order to add songs to the queue.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + final Member selfMember = guild.getSelfMember(); + if (!selfMember.hasPermission(targetChannel, Permission.VOICE_CONNECT) + || !selfMember.hasPermission(targetChannel, Permission.VOICE_SPEAK)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but I do not have the required permissions to use your current voice channel.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (arguments.length == 0) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but you need to provide a search query or a link to a specific track or playlist.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + final TrackScheduler scheduler = AudioUtil.getTrackScheduler(guild); + if (scheduler.isQueueLocked() && !PermissionManager.isWhitelisted(executor)) { + return new CommandResponse("lock", "Sorry " + executor.getAsMention() + ", but the queue is locked.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (scheduler.isQueueFull()) { + return new CommandResponse("card_box", "Sorry " + executor.getAsMention() + ", but the queue is full.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + boolean isLink; + try { + new URL(arguments[0]); + isLink = true; + } catch (final MalformedURLException exception) { + isLink = false; + } + + final StringBuilder query; + if (isLink) { + query = new StringBuilder(arguments[0]); + } else { + query = new StringBuilder("ytsearch:"); + for (final String part : arguments) { + query.append(part).append(" "); + } + } + + AudioUtil.findTrack(query.toString(), executor, channel); + return null; + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/Queue.java b/src/main/java/dev/logal/logalbot/commands/audio/Queue.java new file mode 100644 index 0000000..bbf973c --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/Queue.java @@ -0,0 +1,55 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.TrackUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Queue implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (AudioUtil.getTrackScheduler(guild).isQueueEmpty()) { + return new CommandResponse("information_source", executor.getAsMention() + ", the queue is empty."); + } + + final CommandResponse response = new CommandResponse("bookmark_tabs", + executor.getAsMention() + ", the following tracks are in the queue:"); + + final int page; + if (arguments.length == 0) { + page = 1; + } else { + try { + page = Integer.parseInt(arguments[0]); + } catch (final NumberFormatException exception) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but the page number must be an integer.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + } + + response.attachEmbed( + TrackUtil.generatePaginatedTrackListInfoEmbed(AudioUtil.getTrackScheduler(guild).getQueue(), page)); + return response; + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/Remove.java b/src/main/java/dev/logal/logalbot/commands/audio/Remove.java new file mode 100644 index 0000000..a6c1d68 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/Remove.java @@ -0,0 +1,114 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.vdurmont.emoji.EmojiManager; + +import dev.logal.logalbot.audio.TrackScheduler; +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.commands.ReactionCallback; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.CommandManager; +import dev.logal.logalbot.utils.ReactionCallbackManager; +import dev.logal.logalbot.utils.TrackUtil; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.Permission; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.Message; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Remove implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final TrackScheduler scheduler = AudioUtil.getTrackScheduler(channel.getGuild()); + if (scheduler.isQueueEmpty()) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but there are no tracks in the queue.").setDeletionDelay(10, + TimeUnit.SECONDS); + } + + final Guild guild = channel.getGuild(); + if (AudioUtil.isTrackLoaded(guild) && !VoiceChannelUtil.isInCurrentVoiceChannel(executor)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you need to be in voice channel `" + + AudioUtil.getVoiceChannelConnectedTo(guild).getName() + + "` in order to remove tracks from the queue.").setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (arguments.length == 0) { + if (!guild.getSelfMember().hasPermission(channel, Permission.MESSAGE_ADD_REACTION)) { + return new CommandResponse("no_entry_sign", "Sorry " + executor.getAsMention() + + ", but I do not have the required permissions to create a reaction selection dialog in this text channel.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + final CommandResponse response = new CommandResponse("question", + executor.getAsMention() + ", which track would you like to remove from the top of the queue?"); + response.attachEmbed(TrackUtil.generateTrackListInfoEmbed(scheduler.getQueue(), true)); + + for (int i = 0; i < scheduler.getQueue().size(); i++) { + final int trackNumber = i + 1; + if (trackNumber == 11) { + break; + } + + response.addReactionCallback(EmojiManager.getForAlias("" + trackNumber), new ReactionCallback() { + @Override + public final void run(final Member reactor, final long messageID) { + ReactionCallbackManager.unregisterMessage(messageID); + channel.getMessageById(messageID).queue(this::deleteMessage); + CommandManager.executeCommand(("remove " + trackNumber).split(" "), reactor, channel); + } + + private void deleteMessage(final Message message) { + message.delete().queue(); + } + }); + } + + response.setReactionCallbackTarget(executor); + response.setDeletionDelay(30, TimeUnit.SECONDS); + return response; + } + + final int index; + try { + index = Integer.parseInt(arguments[0]); + } catch (final NumberFormatException exception) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but the index must be an integer.").setDeletionDelay(10, + TimeUnit.SECONDS); + } + + try { + final AudioTrack removedTrack = scheduler.getQueue().get(index - 1); + + scheduler.removeFromQueue(index - 1); + final CommandResponse response = new CommandResponse("scissors", + executor.getAsMention() + " removed the following track from the queue:"); + response.attachEmbed(TrackUtil.generateTrackInfoEmbed(removedTrack)); + return response; + } catch (final IndexOutOfBoundsException exception) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but that index is outside the bounds of the queue.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/Reset.java b/src/main/java/dev/logal/logalbot/commands/audio/Reset.java new file mode 100644 index 0000000..d01222d --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/Reset.java @@ -0,0 +1,46 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Reset implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (AudioUtil.isTrackLoaded(guild) && !VoiceChannelUtil.isInCurrentVoiceChannel(executor)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you need to be in voice channel `" + + AudioUtil.getVoiceChannelConnectedTo(guild).getName() + + "` in order to reset the audio player.").setDeletionDelay(10, TimeUnit.SECONDS); + } + + AudioUtil.getTrackScheduler(guild).clearQueue(); + if (AudioUtil.isTrackLoaded(guild)) { + AudioUtil.stopTrack(guild); + } + + return new CommandResponse("recycle", + executor.getAsMention() + " has stopped the current track and reset the queue."); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/Shuffle.java b/src/main/java/dev/logal/logalbot/commands/audio/Shuffle.java new file mode 100644 index 0000000..c368479 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/Shuffle.java @@ -0,0 +1,49 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import dev.logal.logalbot.audio.TrackScheduler; +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Shuffle implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + final TrackScheduler scheduler = AudioUtil.getTrackScheduler(guild); + if (scheduler.isQueueEmpty()) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but there are no tracks in the queue.").setDeletionDelay(10, + TimeUnit.SECONDS); + } + + if (AudioUtil.isTrackLoaded(guild) && !VoiceChannelUtil.isInCurrentVoiceChannel(executor)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you need to be in voice channel `" + + AudioUtil.getVoiceChannelConnectedTo(guild).getName() + + "` in order to shuffle the queue.").setDeletionDelay(10, TimeUnit.SECONDS); + } + + scheduler.shuffleQueue(); + return new CommandResponse("salad", executor.getAsMention() + " shuffled the queue."); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/Skip.java b/src/main/java/dev/logal/logalbot/commands/audio/Skip.java new file mode 100644 index 0000000..aa7e474 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/Skip.java @@ -0,0 +1,73 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.SkipTracker; +import dev.logal.logalbot.utils.TrackUtil; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Skip implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (!AudioUtil.isTrackLoaded(guild)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but there must be a track playing in order to vote to skip it.").setDeletionDelay(10, + TimeUnit.SECONDS); + } + + if (!VoiceChannelUtil.isInCurrentVoiceChannel(executor)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you must be in voice channel `" + + AudioUtil.getVoiceChannelConnectedTo(channel.getGuild()).getName() + + "` in order to vote to skip the current track.").setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (SkipTracker.hasVoted(executor)) { + return new CommandResponse("no_entry_sign", + "You have already voted to skip the current track " + executor.getAsMention() + ".") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + SkipTracker.registerVote(executor); + if (SkipTracker.shouldSkip(guild)) { + final AudioTrack skippedTrack = AudioUtil.getLoadedTrack(guild); + AudioUtil.getTrackScheduler(guild).skipCurrentTrack(); + final CommandResponse response = new CommandResponse("gun", + executor.getAsMention() + " was the last required vote. The following track has been skipped:"); + response.attachEmbed(TrackUtil.generateTrackInfoEmbed(skippedTrack)); + return response; + } else { + if (SkipTracker.getRemainingRequired(guild) == 1) { + return new CommandResponse("x", executor.getAsMention() + " has voted to skip the current track. " + + SkipTracker.getRemainingRequired(guild) + " more vote is needed."); + } else { + return new CommandResponse("x", executor.getAsMention() + " has voted to skip the current track. " + + SkipTracker.getRemainingRequired(guild) + " more votes are needed."); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/audio/Volume.java b/src/main/java/dev/logal/logalbot/commands/audio/Volume.java new file mode 100644 index 0000000..ca4c372 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/audio/Volume.java @@ -0,0 +1,81 @@ +package dev.logal.logalbot.commands.audio; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.TimeUnit; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.AudioUtil; +import dev.logal.logalbot.utils.VoiceChannelUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Volume implements Command { + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final Guild guild = channel.getGuild(); + if (!AudioUtil.isTrackLoaded(guild)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + + ", but there must be a track playing in order to change the volume of the track player.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + if (!VoiceChannelUtil.isInCurrentVoiceChannel(executor)) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you must be in voice channel `" + + AudioUtil.getVoiceChannelConnectedTo(guild).getName() + + "` in order to change the volume of the track player.").setDeletionDelay(10, + TimeUnit.SECONDS); + } + + if (arguments.length == 0) { + if (AudioUtil.getVolume(guild) >= 75) { + return new CommandResponse("loud_sound", executor.getAsMention() + ", the volume is currently set to `" + + AudioUtil.getVolume(guild) + "%`."); + } else { + return new CommandResponse("sound", executor.getAsMention() + ", the volume is currently set to `" + + AudioUtil.getVolume(guild) + "%`."); + } + } + + final int volume; + try { + volume = Integer.parseInt(arguments[0]); + } catch (final NumberFormatException exception) { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but the volume must be an integer.").setDeletionDelay(10, + TimeUnit.SECONDS); + } + + if (volume <= 150 && volume >= 1) { + final int oldVolume = AudioUtil.getVolume(guild); + AudioUtil.setVolume(guild, volume); + if (volume >= 75) { + return new CommandResponse("loud_sound", + executor.getAsMention() + " set the volume from `" + oldVolume + "%` to `" + volume + "%`."); + } else { + return new CommandResponse("sound", + executor.getAsMention() + " set the volume from `" + oldVolume + "%` to `" + volume + "%`."); + } + } else { + return new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but the volume must be between 1% and 150%.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/fun/EightBall.java b/src/main/java/dev/logal/logalbot/commands/fun/EightBall.java new file mode 100644 index 0000000..68d0ca4 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/fun/EightBall.java @@ -0,0 +1,73 @@ +package dev.logal.logalbot.commands.fun; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.ArrayList; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import com.vdurmont.emoji.EmojiManager; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import dev.logal.logalbot.utils.StringUtil; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class EightBall implements Command { + private final ArrayList responses = new ArrayList<>(20); + private final Random rng = new Random(); + + public EightBall() { + responses.add("It is certain"); + responses.add("It is decidedly so"); + responses.add("Without a doubt"); + responses.add("Yes - definitely"); + responses.add("You may rely on it"); + responses.add("As I see it, yes"); + responses.add("Most likely"); + responses.add("Outlook good"); + responses.add("Yes"); + responses.add("Signs point to yes"); + + responses.add("Reply hazy, try again"); + responses.add("Ask again later"); + responses.add("Better not tell you now"); + responses.add("Cannot predict now"); + responses.add("Concentrate and ask again"); + + responses.add("Don't count on it"); + responses.add("My reply is no"); + responses.add("My sources say no"); + responses.add("Outlook not so good"); + responses.add("Very doubtful"); + } + + @Override + public final CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + if (arguments.length == 0) { + return new CommandResponse("no_entry_sign", + "Sorry, " + executor.getAsMention() + ", but you need to supply a question for the Magic 8 Ball.") + .setDeletionDelay(10, TimeUnit.SECONDS); + } + + final String question = StringUtil.sanitizeCodeBlock(String.join(" ", arguments)); + + return new CommandResponse("question", + executor.getAsMention() + " asked the Magic 8 Ball: `" + question + "`\n" + + EmojiManager.getForAlias("8ball").getUnicode() + " The Magic 8 Ball responds: *" + + responses.get(rng.nextInt(responses.size())) + "*."); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/general/About.java b/src/main/java/dev/logal/logalbot/commands/general/About.java new file mode 100644 index 0000000..c4cc222 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/general/About.java @@ -0,0 +1,28 @@ +package dev.logal.logalbot.commands.general; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class About implements Command { + @Override + public CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + return new CommandResponse("wave", "Hello " + executor.getAsMention() + + "! I'm LogalBot, a bot created by LogalDeveloper. https://logal.dev/"); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/commands/general/Help.java b/src/main/java/dev/logal/logalbot/commands/general/Help.java new file mode 100644 index 0000000..257da73 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/commands/general/Help.java @@ -0,0 +1,34 @@ +package dev.logal.logalbot.commands.general; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import net.dv8tion.jda.core.EmbedBuilder; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; + +public final class Help implements Command { + @Override + public CommandResponse execute(final String[] arguments, final Member executor, final TextChannel channel) { + final CommandResponse response = new CommandResponse("directory", + executor.getAsMention() + ", here are some helpful links:"); + final EmbedBuilder builder = new EmbedBuilder(); + builder.addField("Command Reference", "https://logal.dev/projects/logalbot/command-reference/", false); + builder.addField("Git Repository", "https://git.logal.dev/LogalDeveloper/LogalBot", false); + response.attachEmbed(builder.build()); + return response; + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/events/GuildMessageReactionAdd.java b/src/main/java/dev/logal/logalbot/events/GuildMessageReactionAdd.java new file mode 100644 index 0000000..804521e --- /dev/null +++ b/src/main/java/dev/logal/logalbot/events/GuildMessageReactionAdd.java @@ -0,0 +1,32 @@ +package dev.logal.logalbot.events; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import dev.logal.logalbot.utils.ReactionCallbackManager; +import net.dv8tion.jda.core.events.message.guild.react.GuildMessageReactionAddEvent; +import net.dv8tion.jda.core.hooks.ListenerAdapter; +import net.dv8tion.jda.core.utils.Checks; + +public final class GuildMessageReactionAdd extends ListenerAdapter { + @Override + public final void onGuildMessageReactionAdd(final GuildMessageReactionAddEvent event) { + Checks.notNull(event, "Event"); + + if (!event.getUser().equals(event.getJDA().getSelfUser())) { + ReactionCallbackManager.executeCallback(event.getMessageIdLong(), event.getMember(), + event.getReactionEmote().getName()); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/events/GuildMessageReceived.java b/src/main/java/dev/logal/logalbot/events/GuildMessageReceived.java new file mode 100644 index 0000000..88b03c0 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/events/GuildMessageReceived.java @@ -0,0 +1,77 @@ +package dev.logal.logalbot.events; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.Arrays; +import java.util.List; + +import dev.logal.logalbot.utils.CommandManager; +import dev.logal.logalbot.utils.DataManager; +import net.dv8tion.jda.core.Permission; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.Message; +import net.dv8tion.jda.core.entities.SelfUser; +import net.dv8tion.jda.core.entities.TextChannel; +import net.dv8tion.jda.core.events.message.guild.GuildMessageReceivedEvent; +import net.dv8tion.jda.core.hooks.ListenerAdapter; +import net.dv8tion.jda.core.utils.Checks; + +public final class GuildMessageReceived extends ListenerAdapter { + @Override + public final void onGuildMessageReceived(final GuildMessageReceivedEvent event) { + Checks.notNull(event, "Event"); + + final Guild guild = event.getGuild(); + final Member self = guild.getSelfMember(); + final TextChannel channel = event.getChannel(); + final Message message = event.getMessage(); + if (event.getAuthor().isBot() || message.isTTS() || !self.hasPermission(channel, Permission.MESSAGE_WRITE) + || !self.hasPermission(channel, Permission.MESSAGE_EMBED_LINKS)) { + return; + } + + final String content = message.getContentRaw(); + final SelfUser selfUser = event.getJDA().getSelfUser(); + final List mentionedMembers = message.getMentionedMembers(); + final Member author = event.getMember(); + if (mentionedMembers.size() >= 1 && mentionedMembers.get(0).getUser().getIdLong() == selfUser.getIdLong() + && (content.startsWith(self.getAsMention()) || content.startsWith(selfUser.getAsMention()))) { + final String[] rawCommand = content.split(" "); + final String[] command = Arrays.copyOfRange(rawCommand, 1, rawCommand.length); + if (command.length >= 1) { + if (self.hasPermission(channel, Permission.MESSAGE_MANAGE)) { + message.delete().reason("LogalBot Command Execution").queue(); + } + CommandManager.executeCommand(command, author, channel); + } + } else { + final String commandCharacter = DataManager.getGuildValue(guild, "commandCharacter"); + if (commandCharacter == null) { + return; + } + + final char commandChar = commandCharacter.toCharArray()[0]; + + if (content.length() > 1 && content.charAt(0) == commandChar) { + final String[] command = content.substring(1).split(" "); + if (self.hasPermission(channel, Permission.MESSAGE_MANAGE)) { + message.delete().reason("LogalBot Command Execution").queue(); + } + CommandManager.executeCommand(command, author, channel); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/events/GuildReady.java b/src/main/java/dev/logal/logalbot/events/GuildReady.java new file mode 100644 index 0000000..bcec4a0 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/events/GuildReady.java @@ -0,0 +1,29 @@ +package dev.logal.logalbot.events; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import dev.logal.logalbot.utils.AudioUtil; +import net.dv8tion.jda.core.events.guild.GuildReadyEvent; +import net.dv8tion.jda.core.hooks.ListenerAdapter; +import net.dv8tion.jda.core.utils.Checks; + +public final class GuildReady extends ListenerAdapter { + @Override + public final void onGuildReady(final GuildReadyEvent event) { + Checks.notNull(event, "Event"); + + AudioUtil.initialize(event.getGuild()); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/events/GuildVoiceLeave.java b/src/main/java/dev/logal/logalbot/events/GuildVoiceLeave.java new file mode 100644 index 0000000..bcb1a31 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/events/GuildVoiceLeave.java @@ -0,0 +1,65 @@ +package dev.logal.logalbot.events; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.logal.logalbot.utils.AudioUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.VoiceChannel; +import net.dv8tion.jda.core.events.guild.voice.GuildVoiceLeaveEvent; +import net.dv8tion.jda.core.hooks.ListenerAdapter; +import net.dv8tion.jda.core.utils.Checks; + +public final class GuildVoiceLeave extends ListenerAdapter { + private static final Logger logger = LoggerFactory.getLogger(GuildVoiceLeave.class); + + @Override + public final void onGuildVoiceLeave(final GuildVoiceLeaveEvent event) { + Checks.notNull(event, "Event"); + + final Guild guild = event.getGuild(); + if (!AudioUtil.isAudioConnectionOpen(guild)) { + return; + } + + if (!AudioUtil.isTrackLoaded(guild)) { + return; + } + + final Member member = event.getMember(); + + if (member.getUser().equals(event.getJDA().getSelfUser())) { + return; + } + + final VoiceChannel leftChannel = event.getChannelLeft(); + if (leftChannel.equals(AudioUtil.getVoiceChannelConnectedTo(guild))) { + final List members = leftChannel.getMembers(); + if (members.size() == 1 && members.get(0).getUser().equals(event.getJDA().getSelfUser())) { + logger.info("All listeners left " + leftChannel.getName() + " (" + leftChannel.getId() + ") in " + + guild.getName() + " (" + guild.getId() + ")."); + AudioUtil.getTrackScheduler(guild).clearQueue(); + if (AudioUtil.isTrackLoaded(guild)) { + AudioUtil.stopTrack(guild); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/events/GuildVoiceMove.java b/src/main/java/dev/logal/logalbot/events/GuildVoiceMove.java new file mode 100644 index 0000000..a8db4c5 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/events/GuildVoiceMove.java @@ -0,0 +1,65 @@ +package dev.logal.logalbot.events; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.logal.logalbot.utils.AudioUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.VoiceChannel; +import net.dv8tion.jda.core.events.guild.voice.GuildVoiceMoveEvent; +import net.dv8tion.jda.core.hooks.ListenerAdapter; +import net.dv8tion.jda.core.utils.Checks; + +public final class GuildVoiceMove extends ListenerAdapter { + private static final Logger logger = LoggerFactory.getLogger(GuildVoiceMove.class); + + @Override + public final void onGuildVoiceMove(final GuildVoiceMoveEvent event) { + Checks.notNull(event, "Event"); + + final Guild guild = event.getGuild(); + if (!AudioUtil.isAudioConnectionOpen(guild)) { + return; + } + + if (!AudioUtil.isTrackLoaded(guild)) { + return; + } + + final Member member = event.getMember(); + + if (member.getUser().equals(event.getJDA().getSelfUser())) { + return; + } + + final VoiceChannel leftChannel = event.getChannelLeft(); + if (leftChannel.equals(AudioUtil.getVoiceChannelConnectedTo(guild))) { + final List members = leftChannel.getMembers(); + if (members.size() == 1 && members.get(0).getUser().equals(event.getJDA().getSelfUser())) { + logger.info("All listeners left " + leftChannel.getName() + " (" + leftChannel.getId() + ") in " + + guild.getName() + " (" + guild.getId() + ")."); + AudioUtil.getTrackScheduler(guild).clearQueue(); + if (AudioUtil.isTrackLoaded(guild)) { + AudioUtil.stopTrack(guild); + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/tasks/IdleDisconnectTask.java b/src/main/java/dev/logal/logalbot/tasks/IdleDisconnectTask.java new file mode 100644 index 0000000..b645b34 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/tasks/IdleDisconnectTask.java @@ -0,0 +1,34 @@ +package dev.logal.logalbot.tasks; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import dev.logal.logalbot.utils.AudioUtil; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.utils.Checks; + +public final class IdleDisconnectTask implements Runnable { + private final Guild guild; + + public IdleDisconnectTask(final Guild guild) { + Checks.notNull(guild, "Guild"); + + this.guild = guild; + } + + @Override + public final void run() { + AudioUtil.closeAudioConnection(guild); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/tasks/MessageDeleteTask.java b/src/main/java/dev/logal/logalbot/tasks/MessageDeleteTask.java new file mode 100644 index 0000000..7c8b604 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/tasks/MessageDeleteTask.java @@ -0,0 +1,35 @@ +package dev.logal.logalbot.tasks; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import dev.logal.logalbot.utils.ReactionCallbackManager; +import net.dv8tion.jda.core.entities.Message; +import net.dv8tion.jda.core.utils.Checks; + +public final class MessageDeleteTask implements Runnable { + private final Message messageToDelete; + + public MessageDeleteTask(final Message message) { + Checks.notNull(message, "Message"); + + this.messageToDelete = message; + } + + @Override + public final void run() { + ReactionCallbackManager.unregisterMessage(messageToDelete.getIdLong()); + messageToDelete.delete().queue(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/AudioUtil.java b/src/main/java/dev/logal/logalbot/utils/AudioUtil.java new file mode 100644 index 0000000..3b592cb --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/AudioUtil.java @@ -0,0 +1,173 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.HashMap; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.logal.logalbot.audio.AudioPlayerSendHandler; +import dev.logal.logalbot.audio.TrackLoadHandler; +import dev.logal.logalbot.audio.TrackScheduler; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; +import net.dv8tion.jda.core.entities.VoiceChannel; +import net.dv8tion.jda.core.managers.AudioManager; +import net.dv8tion.jda.core.utils.Checks; + +public final class AudioUtil { + private static final Logger logger = LoggerFactory.getLogger(AudioUtil.class); + + private static final AudioPlayerManager playerManager = new DefaultAudioPlayerManager(); + private static final HashMap players = new HashMap<>(); + private static final HashMap schedulers = new HashMap<>(); + + private AudioUtil() { + // Static access only. + } + + public static final void initializePlayerManager() { + AudioSourceManagers.registerRemoteSources(playerManager); + } + + public static final void initialize(final Guild guild) { + Checks.notNull(guild, "Guild"); + + players.put(guild.getId(), playerManager.createPlayer()); + schedulers.put(guild.getId(), new TrackScheduler(guild)); + players.get(guild.getId()).addListener(schedulers.get(guild.getId())); + + try { + setVolume(guild, Integer.parseInt(DataManager.getGuildValue(guild, "defaultVolume"))); + } catch (final NumberFormatException exception) { + setVolume(guild, 10); + } + + getTrackScheduler(guild).setQueueLocked(false); + setPausedState(guild, false); + + logger.info("Audio environment initialized for " + guild.getName() + " (" + guild.getId() + ")."); + } + + public static final void openAudioConnection(final VoiceChannel channel) { + Checks.notNull(channel, "Channel"); + + final Guild guild = channel.getGuild(); + final AudioManager audioManager = guild.getAudioManager(); + + audioManager.setSendingHandler(new AudioPlayerSendHandler(players.get(guild.getId()))); + audioManager.openAudioConnection(channel); + } + + public static final void closeAudioConnection(final Guild guild) { + Checks.notNull(guild, "Guild"); + + guild.getAudioManager().closeAudioConnection(); + } + + public static final VoiceChannel getVoiceChannelConnectedTo(final Guild guild) { + Checks.notNull(guild, "Guild"); + + return guild.getAudioManager().getConnectedChannel(); + } + + public static final boolean isAudioConnectionOpen(final Guild guild) { + Checks.notNull(guild, "Guild"); + + return guild.getAudioManager().isConnected(); + } + + public static final void playTrack(final Guild guild, final AudioTrack track) { + Checks.notNull(guild, "Guild"); + Checks.notNull(track, "Track"); + + players.get(guild.getId()).playTrack(track); + } + + public static final void stopTrack(final Guild guild) { + Checks.notNull(guild, "Guild"); + + players.get(guild.getId()).stopTrack(); + } + + public static final boolean isTrackLoaded(final Guild guild) { + Checks.notNull(guild, "Guild"); + + return !(getLoadedTrack(guild) == null); + } + + public static final AudioTrack getLoadedTrack(final Guild guild) { + Checks.notNull(guild, "Guild"); + + return players.get(guild.getId()).getPlayingTrack(); + } + + public static final void setPausedState(final Guild guild, final boolean pausedState) { + Checks.notNull(guild, "Guild"); + Checks.notNull(pausedState, "Paused state"); + + guild.getAudioManager().setSelfMuted(pausedState); + players.get(guild.getId()).setPaused(pausedState); + + if (pausedState) { + logger.info("The audio player was paused in " + guild.getName() + " (" + guild.getId() + ")."); + } else { + logger.info("The audio player was resumed in " + guild.getName() + " (" + guild.getId() + ")."); + } + } + + public static final boolean isPlayerPaused(final Guild guild) { + Checks.notNull(guild, "Guild"); + + return players.get(guild.getId()).isPaused(); + } + + public static final void setVolume(final Guild guild, final int volume) { + Checks.notNull(guild, "Guild"); + Checks.positive(volume, "Volume"); + + players.get(guild.getId()).setVolume(volume); + logger.info("The audio player's volume was set to " + getVolume(guild) + "% in " + guild.getName() + " (" + + guild.getId() + ")."); + } + + public static final int getVolume(final Guild guild) { + Checks.notNull(guild, "Guild"); + + return players.get(guild.getId()).getVolume(); + } + + public static final void findTrack(final String query, final Member requester, final TextChannel channel) { + Checks.notNull(query, "Query"); + Checks.notNull(requester, "Requester"); + Checks.notNull(channel, "Channel"); + + playerManager.loadItem(query, new TrackLoadHandler(requester, channel)); + } + + public static final TrackScheduler getTrackScheduler(final Guild guild) { + Checks.notNull(guild, "Guild"); + + return schedulers.get(guild.getId()); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/CommandManager.java b/src/main/java/dev/logal/logalbot/utils/CommandManager.java new file mode 100644 index 0000000..eb5ed21 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/CommandManager.java @@ -0,0 +1,127 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.Arrays; + +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.logal.logalbot.commands.Command; +import dev.logal.logalbot.commands.CommandResponse; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.TextChannel; +import net.dv8tion.jda.core.utils.Checks; + +public final class CommandManager { + private static final Logger logger = LoggerFactory.getLogger(CommandManager.class); + + private static final HashMap commandMap = new HashMap<>(); + private static final HashMap permissionMap = new HashMap<>(); + private static final HashMap aliasMap = new HashMap<>(); + + private CommandManager() { + // Static access only. + } + + public static final void executeCommand(final String[] command, final Member executor, final TextChannel channel) { + Checks.notEmpty(command, "Command"); + Checks.notNull(executor, "Executor"); + Checks.notNull(channel, "Channel"); + + String commandName = command[0].toLowerCase(); + final String[] arguments = Arrays.copyOfRange(command, 1, command.length); + final Guild guild = channel.getGuild(); + CommandResponse response; + + if (aliasMap.containsKey(commandName)) { + commandName = aliasMap.get(commandName); + } + + logger.info(executor.getEffectiveName() + " (" + executor.getUser().getId() + ") executed command '" + + commandName + "' with arguments '" + String.join(" ", arguments) + "' in " + guild.getName() + " (" + + guild.getId() + ")."); + if (!commandMap.containsKey(commandName)) { + response = new CommandResponse("question", + "Sorry " + executor.getAsMention() + ", but I do not know what that command is."); + response.setDeletionDelay(10, TimeUnit.SECONDS); + response.sendResponse(channel); + return; + } + + if (permissionMap.get(commandName) && !PermissionManager.isWhitelisted(executor)) { + logger.info(executor.getEffectiveName() + " (" + executor.getUser().getId() + + ") was denied access to a command due to not being whitelisted in " + guild.getName() + " (" + + guild.getId() + ")."); + response = new CommandResponse("no_entry_sign", + "Sorry " + executor.getAsMention() + ", but you are not allowed to use this command."); + response.setDeletionDelay(10, TimeUnit.SECONDS); + response.sendResponse(channel); + return; + } + + try { + response = commandMap.get(commandName).execute(arguments, executor, channel); + } catch (final Throwable exception) { + logger.error( + "An error occurred while executing a command for " + executor.getEffectiveName() + " (" + + executor.getUser().getId() + ") in " + guild.getName() + " (" + guild.getId() + ").", + exception); + response = new CommandResponse("sos", + "Sorry " + executor.getAsMention() + ", but an error occurred while executing your command!!"); + response.setDeletionDelay(10, TimeUnit.SECONDS); + response.sendResponse(channel); + return; + } + + if (response != null) { + response.sendResponse(channel); + } + } + + public static final void registerCommand(final String command, final Command commandObject, + final boolean mustBeWhitelisted) { + Checks.notEmpty(command, "Command"); + Checks.notNull(commandObject, "Command object"); + Checks.notNull(mustBeWhitelisted, "Whitelist requirement"); + + commandMap.put(command, commandObject); + permissionMap.put(command, mustBeWhitelisted); + } + + public static final void registerCommandAlias(final String alias, final String command) { + Checks.notEmpty(alias, "Alias"); + Checks.notEmpty(command, "Command"); + + aliasMap.put(alias, command); + } + + public static final void unregisterCommand(final String command) { + Checks.notEmpty(command, "Command"); + + commandMap.remove(command); + permissionMap.remove(command); + } + + public static final void unregisterCommandAlias(final String command) { + Checks.notEmpty(command, "Command"); + + aliasMap.remove(command); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/DataManager.java b/src/main/java/dev/logal/logalbot/utils/DataManager.java new file mode 100644 index 0000000..09838ae --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/DataManager.java @@ -0,0 +1,126 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.User; +import net.dv8tion.jda.core.utils.Checks; +import redis.clients.jedis.Jedis; + +public final class DataManager { + private static final Logger logger = LoggerFactory.getLogger(DataManager.class); + + private static final String host = System.getenv("REDIS_HOST"); + private static final String password = System.getenv("REDIS_PASSWORD"); + private static final String databaseNumber = System.getenv("REDIS_DATABASE"); + private static Jedis jedis = new Jedis(); + + private DataManager() { + // Static access only. + } + + public static final void verifyConnection() { + if (!jedis.isConnected()) { + jedis = new Jedis(host); + + if (password != null) { + jedis.auth(password); + } + + if (databaseNumber != null) { + final int num = Integer.parseInt(databaseNumber); + jedis.select(num); + } + } + } + + public static final void runMigrations() { + if (jedis.get("schemaVersion") == null) { + logger.info("Migrating schema to version 1..."); + jedis.set("schemaVersion", "1"); + logger.info("Migration to schema version 1 complete."); + } + } + + public static final String getUserValue(final Member member, final String key) { + Checks.notNull(member, "Member"); + Checks.notEmpty(key, "Key"); + + return jedis.get("g" + member.getGuild().getId() + ":u" + member.getUser().getId() + ":" + key); + } + + public static final void setUserValue(final Member member, final String key, final String value) { + Checks.notNull(member, "Member"); + Checks.notEmpty(key, "Key"); + Checks.notEmpty(value, "Value"); + + jedis.set("g" + member.getGuild().getId() + ":u" + member.getUser().getId() + ":" + key, value); + } + + public static final void deleteUserKey(final Member member, final String key) { + Checks.notNull(member, "Member"); + Checks.notEmpty(key, "Key"); + + jedis.del("g" + member.getGuild().getId() + ":u" + member.getUser().getId() + ":" + key); + } + + public static final String getGlobalUserValue(final User user, final String key) { + Checks.notNull(user, "User"); + Checks.notEmpty(key, "Key"); + + return jedis.get("u" + user.getId() + ":" + key); + } + + public static final void setGlobalUserValue(final User user, final String key, final String value) { + Checks.notNull(user, "User"); + Checks.notEmpty(key, "Key"); + Checks.notEmpty(value, "Value"); + + jedis.set("u" + user.getId() + ":" + key, value); + } + + public static final void deleteGlobalUserKey(final User user, final String key) { + Checks.notNull(user, "User"); + Checks.notEmpty(key, "Key"); + + jedis.del("u" + user.getId() + ":" + key); + } + + public static final String getGuildValue(final Guild guild, final String key) { + Checks.notNull(guild, "Guild"); + Checks.notEmpty(key, "Key"); + + return jedis.get("g" + guild.getId() + ":" + key); + } + + public static final void setGuildValue(final Guild guild, final String key, final String value) { + Checks.notNull(guild, "Guild"); + Checks.notEmpty(key, "Key"); + Checks.notEmpty(value, "Value"); + + jedis.set("g" + guild.getId() + ":" + key, value); + } + + public static final void deleteGuildKey(final Guild guild, final String key) { + Checks.notNull(guild, "Guild"); + Checks.notEmpty(key, "Key"); + + jedis.del("g" + guild.getId() + ":" + key); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/PermissionManager.java b/src/main/java/dev/logal/logalbot/utils/PermissionManager.java new file mode 100644 index 0000000..12026a3 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/PermissionManager.java @@ -0,0 +1,60 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.dv8tion.jda.core.Permission; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.utils.Checks; + +public final class PermissionManager { + private static final Logger logger = LoggerFactory.getLogger(PermissionManager.class); + + private PermissionManager() { + // Static access only. + } + + public static final boolean isWhitelisted(final Member member) { + Checks.notNull(member, "Member"); + + if (member.hasPermission(Permission.ADMINISTRATOR)) { + return true; + } + + if (DataManager.getUserValue(member, "whitelisted") == null) { + DataManager.setUserValue(member, "whitelisted", "false"); + } + + return DataManager.getUserValue(member, "whitelisted").equals("true"); + } + + public static final void addToWhitelist(final Member member) { + Checks.notNull(member, "Member"); + + DataManager.setUserValue(member, "whitelisted", "true"); + logger.info(member.getEffectiveName() + " (" + member.getUser().getId() + ") was added to the whitelist in " + + member.getGuild().getName() + " (" + member.getGuild().getId() + ")."); + } + + public static final void removeFromWhitelist(final Member member) { + Checks.notNull(member, "Member"); + + DataManager.setUserValue(member, "whitelisted", "false"); + logger.info(member.getEffectiveName() + " (" + member.getUser().getId() + ") was removed from the whitelist in " + + member.getGuild().getName() + " (" + member.getGuild().getId() + ")."); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/ReactionCallbackManager.java b/src/main/java/dev/logal/logalbot/utils/ReactionCallbackManager.java new file mode 100644 index 0000000..6be85ca --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/ReactionCallbackManager.java @@ -0,0 +1,80 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.HashMap; + +import com.vdurmont.emoji.Emoji; +import com.vdurmont.emoji.EmojiManager; + +import dev.logal.logalbot.commands.ReactionCallback; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.Message; +import net.dv8tion.jda.core.entities.User; +import net.dv8tion.jda.core.utils.Checks; + +public final class ReactionCallbackManager { + private static final HashMap> callbackDictionary = new HashMap<>(); + private static final HashMap targetDictionary = new HashMap<>(); + + private ReactionCallbackManager() { + // Static access only. + } + + public static final void registerCallback(final Message message, final Emoji emoji, + final ReactionCallback callback) { + Checks.notNull(message, "Message"); + Checks.notNull(emoji, "Emoji"); + Checks.notNull(callback, "Callback"); + + if (!callbackDictionary.containsKey(message.getIdLong())) { + callbackDictionary.put(message.getIdLong(), new HashMap<>()); + } + + callbackDictionary.get(message.getIdLong()).put(emoji, callback); + } + + public static final void setCallbackTarget(final User user, final Message message) { + Checks.notNull(user, "User"); + Checks.notNull(message, "Messsage"); + + targetDictionary.put(message.getIdLong(), user.getIdLong()); + } + + public static final void unregisterMessage(final long messageID) { + Checks.notNull(messageID, "Message ID"); + + callbackDictionary.remove(messageID); + targetDictionary.remove(messageID); + } + + public static final void executeCallback(final long messageID, final Member reactor, final String emoji) { + Checks.notNull(messageID, "Message ID"); + Checks.notNull(reactor, "Reactor"); + Checks.notEmpty(emoji, "Emoji"); + + if (callbackDictionary.containsKey(messageID)) { + if (targetDictionary.containsKey(messageID) + && !targetDictionary.get(messageID).equals(reactor.getUser().getIdLong())) { + return; + } + + final Emoji parsedEmoji = EmojiManager.getByUnicode(emoji); + if (callbackDictionary.get(messageID).containsKey(parsedEmoji)) { + callbackDictionary.get(messageID).get(parsedEmoji).run(reactor, messageID); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/Scheduler.java b/src/main/java/dev/logal/logalbot/utils/Scheduler.java new file mode 100644 index 0000000..3b7c146 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/Scheduler.java @@ -0,0 +1,48 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import net.dv8tion.jda.core.utils.Checks; + +public final class Scheduler { + private static final ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); + + private Scheduler() { + // Static access only. + } + + public static final ScheduledFuture schedule(final Runnable runnable, final long delay, final TimeUnit unit) { + Checks.notNull(runnable, "Runnable"); + Checks.notNull(delay, "Delay"); + Checks.notNull(unit, "Unit"); + + return pool.schedule(runnable, delay, unit); + } + + public static final ScheduledFuture scheduleRepeating(final Runnable runnable, final long initialDelay, + final long period, final TimeUnit unit) { + Checks.notNull(runnable, "Runnable"); + Checks.notNull(initialDelay, "Initial delay"); + Checks.notNull(period, "Period"); + Checks.notNull(unit, "Unit"); + + return pool.scheduleAtFixedRate(runnable, initialDelay, period, unit); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/SkipTracker.java b/src/main/java/dev/logal/logalbot/utils/SkipTracker.java new file mode 100644 index 0000000..df3ba1b --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/SkipTracker.java @@ -0,0 +1,77 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.ArrayList; +import java.util.HashMap; + +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.utils.Checks; + +public final class SkipTracker { + private static final HashMap> skipVotesDictionary = new HashMap<>(); + + private SkipTracker() { + // Static access only. + } + + public static final void registerVote(final Member member) { + Checks.notNull(member, "Member"); + + final Guild guild = member.getGuild(); + if (!skipVotesDictionary.containsKey(guild.getIdLong())) { + resetVotes(guild); + } + + final ArrayList registeredVotes = skipVotesDictionary.get(guild.getIdLong()); + if (!registeredVotes.contains(member.getUser().getIdLong())) { + registeredVotes.add(member.getUser().getIdLong()); + } + } + + public static final boolean hasVoted(final Member member) { + Checks.notNull(member, "Member"); + + return skipVotesDictionary.get(member.getGuild().getIdLong()).contains(member.getUser().getIdLong()); + } + + public static final void resetVotes(final Guild guild) { + Checks.notNull(guild, "Guild"); + + final long guildID = guild.getIdLong(); + if (skipVotesDictionary.containsKey(guildID)) { + skipVotesDictionary.get(guildID).clear(); + } else { + skipVotesDictionary.put(guildID, new ArrayList<>()); + } + } + + public static final int getRemainingRequired(final Guild guild) { + Checks.notNull(guild, "Guild"); + + final int listeners = (int) AudioUtil.getVoiceChannelConnectedTo(guild).getMembers().stream() + .filter(member -> !member.getUser().isBot()).count(); + final int required = (int) Math.ceil(listeners * .55); + + return (required - skipVotesDictionary.get(guild.getIdLong()).size()); + } + + public static final boolean shouldSkip(final Guild guild) { + Checks.notNull(guild, "Guild"); + + return getRemainingRequired(guild) <= 0; + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/StringUtil.java b/src/main/java/dev/logal/logalbot/utils/StringUtil.java new file mode 100644 index 0000000..c1907a7 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/StringUtil.java @@ -0,0 +1,45 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import net.dv8tion.jda.core.utils.Checks; + +public final class StringUtil { + private StringUtil() { + // Static access only. + } + + public static final String sanitize(final String string) { + Checks.notNull(string, "String"); + + return string.replaceAll("([_*`<@>~|])", "\\\\$1").replaceAll("[\r\n]", ""); + } + + public static final String sanitizeCodeBlock(final String string) { + Checks.notNull(string, "String"); + + return string.replaceAll("[`]", "'").replaceAll("[\r\n]", ""); + } + + public static final String formatTime(final long milliseconds) { + Checks.notNull(milliseconds, "Milliseconds"); + + final long second = (milliseconds / 1000) % 60; + final long minute = (milliseconds / (1000 * 60)) % 60; + final long hour = (milliseconds / (1000 * 60 * 60)) % 24; + + return String.format("%02d:%02d:%02d", hour, minute, second); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/TrackUtil.java b/src/main/java/dev/logal/logalbot/utils/TrackUtil.java new file mode 100644 index 0000000..bdcdfb7 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/TrackUtil.java @@ -0,0 +1,110 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import java.util.List; + +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import net.dv8tion.jda.core.EmbedBuilder; +import net.dv8tion.jda.core.entities.Guild; +import net.dv8tion.jda.core.entities.MessageEmbed; +import net.dv8tion.jda.core.utils.Checks; + +public final class TrackUtil { + private TrackUtil() { + // Static access only. + } + + public static final MessageEmbed generateTrackInfoEmbed(final AudioTrack track) { + Checks.notNull(track, "Track"); + + final EmbedBuilder builder = new EmbedBuilder(); + builder.addField(StringUtil.sanitize(track.getInfo().title), + StringUtil.sanitize(track.getInfo().author) + " - " + StringUtil.formatTime(track.getDuration()), + false); + return builder.build(); + } + + public static final MessageEmbed generateCurrentTrackInfoEmbed(final Guild guild) { + Checks.notNull(guild, "Guild"); + + final EmbedBuilder builder = new EmbedBuilder(); + final AudioTrack track = AudioUtil.getLoadedTrack(guild); + builder.addField( + StringUtil.sanitize(track.getInfo().title), StringUtil.sanitize(track.getInfo().author) + " - " + + StringUtil.formatTime(track.getPosition()) + "/" + StringUtil.formatTime(track.getDuration()), + false); + return builder.build(); + } + + public static final MessageEmbed generateTrackListInfoEmbed(final List tracks, final boolean numbered) { + Checks.notNull(tracks, "Tracks"); + Checks.notNull(numbered, "Numbered"); + + final EmbedBuilder builder = new EmbedBuilder(); + for (int i = 0; i < tracks.size(); i++) { + if (i == 10) { + break; + } + + final AudioTrack track = tracks.get(i); + if (numbered) { + builder.addField("**" + (i + 1) + ":** " + StringUtil.sanitize(track.getInfo().title), + StringUtil.sanitize(track.getInfo().author) + " - " + + StringUtil.formatTime(track.getDuration()), + false); + } else { + builder.addField(StringUtil.sanitize(track.getInfo().title), StringUtil.sanitize(track.getInfo().author) + + " - " + StringUtil.formatTime(track.getDuration()), false); + } + } + + if (tracks.size() > 10) { + builder.setTitle("**Top 10 Tracks - " + (tracks.size() - 10) + " Not Shown**"); + } + return builder.build(); + } + + public static final MessageEmbed generatePaginatedTrackListInfoEmbed(final List tracks, int page) { + Checks.notNull(tracks, "Tracks"); + Checks.notNull(page, "Page"); + + final EmbedBuilder builder = new EmbedBuilder(); + if (page < 1) { + page = 1; + } + + final int pages = (int) Math.ceil(tracks.size() / 10d); + + if (page > pages) { + page = pages; + } + + page = page - 1; + final int start = page * 10; + final int end = start + 10; + + for (int i = start; i < end && i < tracks.size(); i++) { + final AudioTrack track = tracks.get(i); + builder.addField("**" + (i + 1) + ":** " + StringUtil.sanitize(track.getInfo().title), + StringUtil.sanitize(track.getInfo().author) + " - " + StringUtil.formatTime(track.getDuration()), + false); + } + + builder.setTitle("**" + tracks.size() + " Total Tracks - Page " + (page + 1) + "/" + pages + "**"); + return builder.build(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/logal/logalbot/utils/VoiceChannelUtil.java b/src/main/java/dev/logal/logalbot/utils/VoiceChannelUtil.java new file mode 100644 index 0000000..45a9b03 --- /dev/null +++ b/src/main/java/dev/logal/logalbot/utils/VoiceChannelUtil.java @@ -0,0 +1,42 @@ +package dev.logal.logalbot.utils; + +// Copyright 2019 Logan Fick + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import net.dv8tion.jda.core.entities.Member; +import net.dv8tion.jda.core.entities.VoiceChannel; +import net.dv8tion.jda.core.utils.Checks; + +public final class VoiceChannelUtil { + private VoiceChannelUtil() { + // Static access only. + } + + public static final VoiceChannel getVoiceChannelMemberIsConnectedTo(final Member member) { + Checks.notNull(member, "Member"); + + for (final VoiceChannel channel : member.getGuild().getVoiceChannels()) { + if (channel.getMembers().contains(member)) { + return channel; + } + } + return null; + } + + public static final boolean isInCurrentVoiceChannel(final Member member) { + Checks.notNull(member, "Member"); + + return AudioUtil.getVoiceChannelConnectedTo(member.getGuild()).getMembers().contains(member); + } +} \ No newline at end of file