From b40dd8b9b02dc99e03872303c2b317d29286346f Mon Sep 17 00:00:00 2001 From: Logan Fick Date: Sat, 25 May 2019 16:06:57 -0400 Subject: [PATCH] Made LogalBot open source. --- .gitignore | 163 ++++++++++++++ .vscode/launch.json | 15 ++ LICENSE.txt | 176 +++++++++++++++ build.gradle | 31 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55627 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 +++++++++++++++ gradlew.bat | 84 ++++++++ settings.gradle | 1 + src/main/java/dev/logal/logalbot/Main.java | 141 ++++++++++++ .../audio/AudioPlayerSendHandler.java | 48 +++++ .../logalbot/audio/TrackLoadHandler.java | 201 ++++++++++++++++++ .../logal/logalbot/audio/TrackScheduler.java | 176 +++++++++++++++ .../dev/logal/logalbot/commands/Command.java | 22 ++ .../logalbot/commands/CommandResponse.java | 92 ++++++++ .../logalbot/commands/ReactionCallback.java | 21 ++ .../commands/administration/Settings.java | 123 +++++++++++ .../commands/administration/Whitelist.java | 82 +++++++ .../logalbot/commands/audio/ForceSkip.java | 56 +++++ .../logal/logalbot/commands/audio/Lock.java | 55 +++++ .../logalbot/commands/audio/NowPlaying.java | 38 ++++ .../logal/logalbot/commands/audio/Pause.java | 53 +++++ .../logal/logalbot/commands/audio/Play.java | 100 +++++++++ .../logal/logalbot/commands/audio/Queue.java | 55 +++++ .../logal/logalbot/commands/audio/Remove.java | 114 ++++++++++ .../logal/logalbot/commands/audio/Reset.java | 46 ++++ .../logalbot/commands/audio/Shuffle.java | 49 +++++ .../logal/logalbot/commands/audio/Skip.java | 73 +++++++ .../logal/logalbot/commands/audio/Volume.java | 81 +++++++ .../logalbot/commands/fun/EightBall.java | 73 +++++++ .../logalbot/commands/general/About.java | 28 +++ .../logal/logalbot/commands/general/Help.java | 34 +++ .../events/GuildMessageReactionAdd.java | 32 +++ .../logalbot/events/GuildMessageReceived.java | 77 +++++++ .../dev/logal/logalbot/events/GuildReady.java | 29 +++ .../logalbot/events/GuildVoiceLeave.java | 65 ++++++ .../logal/logalbot/events/GuildVoiceMove.java | 65 ++++++ .../logalbot/tasks/IdleDisconnectTask.java | 34 +++ .../logalbot/tasks/MessageDeleteTask.java | 35 +++ .../dev/logal/logalbot/utils/AudioUtil.java | 173 +++++++++++++++ .../logal/logalbot/utils/CommandManager.java | 127 +++++++++++ .../dev/logal/logalbot/utils/DataManager.java | 126 +++++++++++ .../logalbot/utils/PermissionManager.java | 60 ++++++ .../utils/ReactionCallbackManager.java | 80 +++++++ .../dev/logal/logalbot/utils/Scheduler.java | 48 +++++ .../dev/logal/logalbot/utils/SkipTracker.java | 77 +++++++ .../dev/logal/logalbot/utils/StringUtil.java | 45 ++++ .../dev/logal/logalbot/utils/TrackUtil.java | 110 ++++++++++ .../logalbot/utils/VoiceChannelUtil.java | 42 ++++ 49 files changed, 3633 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 LICENSE.txt create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/dev/logal/logalbot/Main.java create mode 100644 src/main/java/dev/logal/logalbot/audio/AudioPlayerSendHandler.java create mode 100644 src/main/java/dev/logal/logalbot/audio/TrackLoadHandler.java create mode 100644 src/main/java/dev/logal/logalbot/audio/TrackScheduler.java create mode 100644 src/main/java/dev/logal/logalbot/commands/Command.java create mode 100644 src/main/java/dev/logal/logalbot/commands/CommandResponse.java create mode 100644 src/main/java/dev/logal/logalbot/commands/ReactionCallback.java create mode 100644 src/main/java/dev/logal/logalbot/commands/administration/Settings.java create mode 100644 src/main/java/dev/logal/logalbot/commands/administration/Whitelist.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/ForceSkip.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/Lock.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/NowPlaying.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/Pause.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/Play.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/Queue.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/Remove.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/Reset.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/Shuffle.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/Skip.java create mode 100644 src/main/java/dev/logal/logalbot/commands/audio/Volume.java create mode 100644 src/main/java/dev/logal/logalbot/commands/fun/EightBall.java create mode 100644 src/main/java/dev/logal/logalbot/commands/general/About.java create mode 100644 src/main/java/dev/logal/logalbot/commands/general/Help.java create mode 100644 src/main/java/dev/logal/logalbot/events/GuildMessageReactionAdd.java create mode 100644 src/main/java/dev/logal/logalbot/events/GuildMessageReceived.java create mode 100644 src/main/java/dev/logal/logalbot/events/GuildReady.java create mode 100644 src/main/java/dev/logal/logalbot/events/GuildVoiceLeave.java create mode 100644 src/main/java/dev/logal/logalbot/events/GuildVoiceMove.java create mode 100644 src/main/java/dev/logal/logalbot/tasks/IdleDisconnectTask.java create mode 100644 src/main/java/dev/logal/logalbot/tasks/MessageDeleteTask.java create mode 100644 src/main/java/dev/logal/logalbot/utils/AudioUtil.java create mode 100644 src/main/java/dev/logal/logalbot/utils/CommandManager.java create mode 100644 src/main/java/dev/logal/logalbot/utils/DataManager.java create mode 100644 src/main/java/dev/logal/logalbot/utils/PermissionManager.java create mode 100644 src/main/java/dev/logal/logalbot/utils/ReactionCallbackManager.java create mode 100644 src/main/java/dev/logal/logalbot/utils/Scheduler.java create mode 100644 src/main/java/dev/logal/logalbot/utils/SkipTracker.java create mode 100644 src/main/java/dev/logal/logalbot/utils/StringUtil.java create mode 100644 src/main/java/dev/logal/logalbot/utils/TrackUtil.java create mode 100644 src/main/java/dev/logal/logalbot/utils/VoiceChannelUtil.java 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 0000000000000000000000000000000000000000..9ab0a835890a5db3e8ae4f8159e0cee6a2f64f45 GIT binary patch literal 55627 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 AF#rGn literal 0 HcmV?d00001 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