diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9eab85a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 diff --git a/.github/workflows/lint-code.yml b/.github/workflows/lint-code.yml new file mode 100644 index 0000000..6e33a6b --- /dev/null +++ b/.github/workflows/lint-code.yml @@ -0,0 +1,22 @@ +name: Lint +on: [push, pull_request] +jobs: + lint: + name: ktlint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2.3.4 + + - name: Install java + uses: actions/setup-java@v1.4.3 + with: + java-version: '13.0.2' # The JDK version to make available on the path. + java-package: jdk # (jre, jdk, or jdk+fx) - defaults to jdk + architecture: x64 # (x64 or x86) - defaults to x64 + + - name: Install dependencies + run: ./gradlew dependencies + + - name: run linter + run: ./gradlew lintKotlin diff --git a/.gitignore b/.gitignore index a1c2a23..dec3bf0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,13 @@ -# 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* +.idea/ +!\.idea/copyright/* +!\.idea/copyright/yuuto.xml +!\.idea/copyright/profiles_settings.xml +.gradle/ +out/ +build/ +.env +*.iml +*.ipr +*.iws +rigged_ships.json5 +owoify.bundle.js diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..f609008 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/yuuto.xml b/.idea/copyright/yuuto.xml new file mode 100644 index 0000000..a50ea8f --- /dev/null +++ b/.idea/copyright/yuuto.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index 6c84a50..86e0f59 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The first command clones over HTTPS, the second one over SSH, thus requiring you - [Project Setup](#project-setup) - [Bot application](#bot-application) - [Development](#development) + - [Intents](#intents) - [Development Server](#development-server) - [Bots](#bots) - [Channels](#channels) @@ -44,6 +45,23 @@ The first command clones over HTTPS, the second one over SSH, thus requiring you Kyuuto bot is written in Kotlin and the repository hosted on GitHub, as it's the most popular platform to host Open Source project repositories. The decision for Kotlin came from the initial JS dev team's wish for a more robust platform than Node.js. +To get started setting up the project you will need the following things: +1. Intellij IDEA (The free version supports everything we need) +2. Java 13, added to path and JAVA_HOME env var set, adopt openjdk hotspot recommended +3. A bit of knowledge about gradle +4. Knowledge about kotlin + +There are a couple of important gradle commands that you need to know as well, these commands are: +- `./gradlew run` - Runs the project +- `./gradlew lintKotlin` - Runs the linter +- `./gradlew formatKotlin` - Fixes code styling (can be done with ctrl-alt-l in intellij as well) +- `./gradlew build` - Builds the project into a jar + +If you are setting up the project for the first time you will need to run `./gradlew idea`. +This command will make sure you have all the proper files for intellij to function correctly. Alternatively you can also run `./gradlew openIdea` to open your intellij installation from the command line directly. + +Note: If you are a Windows user you will need to use `gradlew.bat` instead of `./gradlew` + ### Bot application The bot is developed using Kotlin, for JDK 13. @@ -53,6 +71,16 @@ Once you have cloned the repository on your local machine, make sure to set up y Yuuto bot has its own development server, you can join it by clicking [here](https://discord.gg/fPFbV8G). The server is the official means of discussion and collaboration on the bot, together with GitHub's collaboration tools. +### Intents + +Because Yuuto is using the `GUILD_MEMBERS` gateway intent from discord you must enable this in your developer portal. + +To enable this you have to follow the following steps: +1. Go to https://discordapp.com/developers/applications and select the application that you want to run the code on. +2. Click on the application and select "Bot" on the left side. +3. When you are on the bot page scroll down to "Privileged Gateway Intents" and enable "SERVER MEMBERS INTENT". +4. Press the save button and you are good to go to run Yuuto. + ## Development Server The development server is the place where the campers can interact and test the bot, as for many it might be easier than to work with GitHub's integrated tools and branches. The server also makes use of webhooks to make integration with GitHub even simpler. diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..3a89522 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,100 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import org.gradle.api.tasks.wrapper.Wrapper.DistributionType + +plugins { + idea + application + kotlin("jvm") version "1.4.32" + id("org.jmailen.kotlinter") version "3.4.0" + id("com.github.johnrengelman.shadow") version "6.0.0" +} + +project.group = "io.github.yuutoproject" +project.version = "4.0-alpha" + +repositories { + mavenCentral() + maven("https://m2.dv8tion.net/releases") + jcenter() // JDA utils is still on here :/ +} + +// JDA and logback-classic are written in java +// But kotlin has terrific java interop +dependencies { + // Kotlin STD and other kotlin stuff + implementation(kotlin("stdlib-jdk8")) + implementation(group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "1.4.3") + // The discord lib + implementation(group = "net.dv8tion", name = "JDA", version = "4.2.1_253") { + exclude(module = "opus-java") + } + // Utils (aka finder util) + implementation(group = "com.jagrosh", name = "jda-utilities", version = "3.0.4") + // dotenv support + implementation(group = "io.github.cdimascio", name = "java-dotenv", version = "5.1.3") + // For logging + implementation(group = "ch.qos.logback", name = "logback-classic", version = "1.2.3") + // For loading the commands (super small lib) + implementation(group = "org.reflections", name = "reflections", version = "0.9.12") + // Http client + implementation(group = "com.squareup.okhttp3", name = "okhttp", version = "4.9.0") + // Json library, ships with JDA but is not in our classpath until we list it here + implementation(group = "com.fasterxml.jackson.core", name = "jackson-databind", version = "2.10.1") + // For all conversions + implementation(group = "org.jscience", name = "jscience", version = "4.3.1") +} + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "11" + } + compileTestKotlin { + kotlinOptions.jvmTarget = "11" + } + wrapper { + gradleVersion = "6.5" + distributionType = DistributionType.ALL + } + shadowJar { + archiveClassifier.set("shadow") + + manifest { + attributes["Description"] = "Kyuuto is cute tho" + } + } +} + +// Force a minimum of java 11 +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +application { + mainClassName = "${project.group}.yuutobot.YuutoKt" +} + +kotlinter { + ignoreFailures = false + indentSize = 4 + reporters = arrayOf("checkstyle", "plain") + experimentalRules = true + disabledRules = arrayOf("no-wildcard-imports", "experimental:indent", "experimental:argument-list-wrapping") +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1ebe9fc --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# +# Open source bot built by and for the Camp Buddy Discord Fan Server. +# Copyright (C) 2020 Kyuuto-devs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..490fda8 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..186b715 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..2fe81a7 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for 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='"-Xmx64m" "-Xms64m"' + +# 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..9109989 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto 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.kts b/settings.gradle.kts new file mode 100644 index 0000000..03693e4 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,19 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +rootProject.name = "kyuuto" diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/CommandManager.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/CommandManager.kt new file mode 100644 index 0000000..3166063 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/CommandManager.kt @@ -0,0 +1,105 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot + +import io.github.yuutoproject.yuutobot.commands.Help +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import org.reflections.Reflections +import org.slf4j.LoggerFactory +import java.lang.reflect.Modifier + +class CommandManager { + private val logger = LoggerFactory.getLogger(this.javaClass) + + val commands = hashMapOf() + val aliases = hashMapOf() + val prefix = config.get("PREFIX", "!") + + init { + loadCommands() + } + + private fun register(instance: AbstractCommand) { + commands[instance.name] = instance + + instance.aliases.forEach { alias -> + aliases[alias] = instance.name + } + } + + private fun loadCommands() { + register(Help(this)) + + val reflections = Reflections("io.github.yuutoproject.yuutobot.commands") + + reflections.getSubTypesOf(AbstractCommand::class.java) + .filter { !Modifier.isAbstract(it.modifiers) && it.declaredConstructors[0].parameters.isEmpty() } + .forEach { + register(it.getDeclaredConstructor().newInstance()) + } + } + + fun handleMessage(event: GuildMessageReceivedEvent) { + val author = event.author + val content = event.message.contentRaw + + if (event.isWebhookMessage || author.isBot || content.isBlank() || !content.toLowerCase().startsWith(prefix)) { + return + } + + val args = content.substring(prefix.length) + .trim() + .split("\\s+".toRegex()) + .toMutableList() + + // Interesting case + if (args.isEmpty()) { + return + } + + val invoke = args.removeAt(0).toLowerCase() + + // If no command exists with that name, ignore the message + val command = getCommand(invoke) ?: return + + val commandName = command.name + + // Run the commands asynchronously so they don't block the event thread + GlobalScope.launch { + logger.info("Running command $commandName in ${event.guild} with $args") + + try { + command.run(args, event) + } catch (e: Throwable) { + event.channel.sendMessage("${author.asMention}, there was an error trying to execute that command!") + .queue() + logger.error("Command $commandName failed in ${event.guild} with $args", e) + } + } + } + + fun getCommand(commandName: String) = commands[commandName] ?: if (aliases.contains(commandName)) { + commands[aliases[commandName]] + } else { + null + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/Listener.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/Listener.kt new file mode 100644 index 0000000..20a6933 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/Listener.kt @@ -0,0 +1,46 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot + +import net.dv8tion.jda.api.events.GenericEvent +import net.dv8tion.jda.api.events.ReadyEvent +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import net.dv8tion.jda.api.hooks.EventListener +import org.slf4j.LoggerFactory + +class Listener : EventListener { + private val logger = LoggerFactory.getLogger(this.javaClass) + private val commandManager = CommandManager() + + override fun onEvent(event: GenericEvent) { + if (event is GuildMessageReceivedEvent) { + val botcmdsChannel = config["BOTCMDS_${event.guild.idLong}"] + + // If the botcms channel is set for the server and the channel is the channel stored + // Commands can be executed, if not this will return + if (botcmdsChannel != null && botcmdsChannel != event.channel.id) { + return + } + + commandManager.handleMessage(event) + } else if (event is ReadyEvent) { + logger.info("Logged in as {}", event.jda.selfUser.asTag) + } + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/Utils.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/Utils.kt new file mode 100644 index 0000000..1ab3399 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/Utils.kt @@ -0,0 +1,26 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot + +object Utils { + fun hexStringToInt(hex: String): Int { + val hexValue = "0x" + hex.replaceFirst("#".toRegex(), "") + return Integer.decode(hexValue) + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/Yuuto.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/Yuuto.kt new file mode 100644 index 0000000..40de552 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/Yuuto.kt @@ -0,0 +1,56 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot + +import io.github.cdimascio.dotenv.Dotenv +import net.dv8tion.jda.api.GatewayEncoding +import net.dv8tion.jda.api.JDABuilder +import net.dv8tion.jda.api.entities.Activity +import net.dv8tion.jda.api.requests.GatewayIntent +import net.dv8tion.jda.api.utils.ChunkingFilter +import net.dv8tion.jda.api.utils.MemberCachePolicy +import net.dv8tion.jda.api.utils.cache.CacheFlag.* +import org.slf4j.LoggerFactory + +val config = Dotenv.load() + +fun main() { + val logger = LoggerFactory.getLogger("YuutoKt") + + // Print something with the logger + // You can also use println but that does not look as nice + logger.info("Booting YuutoKt") + logger.info("Prefix is {}", config["PREFIX"]) + + JDABuilder.createDefault( + config["TOKEN"], + GatewayIntent.GUILD_MEMBERS, + GatewayIntent.GUILD_MESSAGES, + GatewayIntent.GUILD_MESSAGE_REACTIONS + ) + .addEventListeners(Listener()) + .disableCache(ACTIVITY, CLIENT_STATUS, EMOTE, VOICE_STATE) + .setChunkingFilter(ChunkingFilter.ALL) + .setMemberCachePolicy(MemberCachePolicy.ALL) + .setActivity(Activity.playing("volleyball")) + .setBulkDeleteSplittingEnabled(false) + // ETF is more optimized + .setGatewayEncoding(GatewayEncoding.ETF) + .build() +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Avatar.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Avatar.kt new file mode 100644 index 0000000..33043a1 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Avatar.kt @@ -0,0 +1,47 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import io.github.yuutoproject.yuutobot.utils.findMember +import net.dv8tion.jda.api.entities.User +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent + +class Avatar : AbstractCommand( + "avatar", + CommandCategory.UTILITIES, + "Gets your own or someone's avatar", + "[user]" +) { + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + var user: User? = event.author + + if (args.isNotEmpty()) { + user = findMember(args.joinToString(" "), event)?.user + } + + if (user == null) { + event.channel.sendMessage("${event.author.asMention} Sorry, but I can't find that user").queue() + return + } + + event.channel.sendMessage("${event.author.asMention}, Here ya go~!\n" + user.effectiveAvatarUrl + "?size=2048").queue() + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Cvt.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Cvt.kt new file mode 100644 index 0000000..8114673 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Cvt.kt @@ -0,0 +1,186 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import javax.measure.Measure +import javax.measure.unit.NonSI.* +import javax.measure.unit.SI.* +import kotlin.math.pow + +class Cvt : AbstractCommand( + "cvt", + CommandCategory.UTILITIES, + "Convert different lengths, weights, and temperatures", + " " +) { + override val aliases = arrayOf("convert") + + private val inputPattern = "(-?[\\d.]+)(\\D{1,3})".toRegex() + private val lengths = arrayOf("mm", "cm", "m", "pc", "pt", "in", "ft", "px") + private val temps = arrayOf("c", "f", "k") + private val weights = arrayOf("kg", "lbs") + + // The + sign combines the arrays + private val validUnits = temps + lengths + weights + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + val channel = event.channel + + if (args.isNotEmpty() && args[0].toLowerCase() == "handegg") { + channel.sendMessage("Americans call the handegg a football.").queue() + return + } + + if (args.size < 2) { + channel.sendMessage( + "Temperature units to convert to are `${temps.joinToString("`, `")}` from those values.\n" + + "Height units to convert to are `${lengths.joinToString("`, `")}` from those same values as well.\n" + + "Weight units to convert to are `${weights.joinToString("`, `")}` again from the same values.\n" + + "The syntax is `cvt `" + ).queue() + return + } + + val targetUnit = args[0].toLowerCase() + + if (!validUnits.contains(targetUnit)) { + channel.sendMessage(" Valid units are `${validUnits.joinToString("`, `")}`") + .queue() + return + } + + val input = args[1].toLowerCase() + + if (!input.matches(inputPattern)) { + channel.sendMessage("<:NatsumiThink:701311512714805279> Not sure what you mean by `$input`.").queue() + return + } + + val inputSplit = inputPattern.find(input) ?: return + // Destructuring the list + val (_, sourceValue, sourceUnit) = inputSplit.groupValues + val srcUnitLower = sourceUnit.toLowerCase() + + if (!srcUnitLower.isCompatibleWithUnit(targetUnit)) { + channel.sendMessage("<:YoichiLOL:701312070880329800> I wish that that was possible as well mate.").queue() + return + } + + val sourceFloat = sourceValue.toFloat() + + if ((lengths.contains(targetUnit) || weights.contains(targetUnit)) && sourceFloat < 0) { + channel.sendMessage("<:AmaThink:701049739747000371> I don't think that `$input` is possible").queue() + return + } + + val converted = when { + lengths.contains(targetUnit) -> convertLength(sourceFloat, sourceUnit, targetUnit) + weights.contains(targetUnit) -> { + val source = sourceUnit.toMassUnit() + val target = targetUnit.toMassUnit() + val converter = source.getConverterTo(target) + val measure = Measure.valueOf(sourceFloat, source).doubleValue(source) + + converter.convert(measure).toFloat() + } + else -> { + val kelvin = sourceFloat.toKelvin(srcUnitLower) + + if (kelvin < 0 || kelvin > 10F.pow(32F)) { + val highLow = if (kelvin < 0) "low" else "high" + + channel.sendMessage("<:HiroOhGod:701312362401103902> Temperatures that $highLow are not possible.") + .queue() + return + } + + kelvin.toTemp(targetUnit) + } + } + + val aboutPrecise = if (srcUnitLower == targetUnit) "precisely" else "about" + + channel.sendMessage( + "<:LeeCute:701312766115315733> According to my calculations, " + + "`$sourceFloat${srcUnitLower.displayUnit()}` is $aboutPrecise `$converted${targetUnit.displayUnit()}`" + ) + .queue() + } + + private fun String.isCompatibleWithUnit(unit: String): Boolean { + return temps.contains(this) && temps.contains(unit) || + weights.contains(this) && weights.contains(unit) || + lengths.contains(this) && lengths.contains(unit) + } + + private fun String.displayUnit() = when (this) { + "c" -> "\u2103" + "f" -> "\u00B0\u0046" + "k" -> "K" // Upper case K + else -> this + } + + private fun Float.toKelvin(srcUnit: String) = when (srcUnit) { + "c" -> this + 273.15F + "f" -> (this - 32F) * (5F / 9F) + 273.15F + "k" -> this + else -> throw IllegalArgumentException("Invalid temperature supplied") // Should never happen + } + + private fun Float.toTemp(targetUnit: String) = when (targetUnit) { + "c" -> this - 273.15F + "f" -> (this - 273.15F) * (9F / 5F) + 32F + "k" -> this + else -> throw IllegalArgumentException("Invalid temperature supplied") // Should never happen + } + + private fun convertLength(num: Float, source: String, target: String): Float { + val sourceUnit = source.toLengthUnit() + val targetUnit = target.toLengthUnit() + val converter = sourceUnit.getConverterTo(targetUnit) + val measure = Measure.valueOf(num, sourceUnit).doubleValue(sourceUnit) + + return converter.convert(measure).toFloat() + } + + private fun String.toLengthUnit() = when (this) { + "mm" -> MILLIMETER + "cm" -> CENTIMETER + "m" -> METER + "km" -> KILOMETER + + "pc" -> POINT.times(12) + "pt" -> POINT + "in" -> INCH + "ft" -> FOOT + "px" -> PIXEL + + else -> throw IllegalArgumentException("Unit not found") + } + + private fun String.toMassUnit() = when (this) { + "kg" -> KILOGRAM + "lbs" -> POUND + + else -> throw IllegalArgumentException("Unit not found") + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Dialog.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Dialog.kt new file mode 100644 index 0000000..9c0ee25 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Dialog.kt @@ -0,0 +1,211 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import com.fasterxml.jackson.core.type.TypeReference +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import io.github.yuutoproject.yuutobot.extensions.JSON_TYPE +import io.github.yuutoproject.yuutobot.extensions.isDeveloper +import io.github.yuutoproject.yuutobot.utils.EMOJI_REGEX +import io.github.yuutoproject.yuutobot.utils.NONASCII_REGEX +import io.github.yuutoproject.yuutobot.utils.httpClient +import io.github.yuutoproject.yuutobot.utils.jackson +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import net.dv8tion.jda.api.utils.data.DataObject +import net.dv8tion.jda.internal.utils.IOUtil +import okhttp3.Call +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import java.io.IOException +import okhttp3.Callback as OkHttp3Callback + +class Dialog : AbstractCommand( + "dialog", + CommandCategory.INFO, + "Generates an image of a character in Camp Buddy saying anything you want", + "[bg] " +) { + private val backgrounds: MutableList + private val characters: MutableList + + init { + val bgAndChars = getBackgroundsAndCharacters() + + backgrounds = bgAndChars.first + characters = bgAndChars.second + } + + private val backgroundsString = "`${backgrounds.joinToString("`, `")}`" + private val charactersString = "`${characters.joinToString("`, `")}`" + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + val channel = event.channel + + if (args.size == 1) { + when (args[0].toLowerCase()) { + "list" -> { + channel.sendMessage( + """Here are the lists of characters and backgrounds that you can use: + | + |Characters: $charactersString + | + |Backgrounds: $backgroundsString + """.trimMargin() + ).queue() + return + } + "reload" -> { + if (event.member!!.isDeveloper) { + val bgAndChars = getBackgroundsAndCharacters() + + backgrounds.clear() + characters.clear() + + backgrounds.addAll(bgAndChars.first) + characters.addAll(bgAndChars.second) + + channel.sendMessage("Reloaded!").queue() + return + } + } + } + } + + if (args.size < 2) { + channel.sendMessage("This command requires at least two arguments : `dialog [background] ` ([] is optional)") + .queue() + return + } + + var character = args.removeAt(0).toLowerCase() + val background: String + + if (characters.contains(character)) { + background = "camp" + } else { + background = character + character = args.removeAt(0).toLowerCase() + } + + if (!backgrounds.contains(background)) { + channel.sendMessage("Sorry, but I couldn't find `$background` as a location\nAvailable backgrounds are: $backgroundsString") + .queue() + return + } + + if (!characters.contains(character)) { + channel.sendMessage("Sorry, but I don't think that `$character` is a character in Camp Buddy\nAvailable characters are: $charactersString") + .queue() + return + } + + if (args.count() < 1) { + channel.sendMessage("Please provide a message to be written~!").queue() + return + } + + val text = args.joinToString(" ").replace("/[‘’]/g".toRegex(), "'") + + if (text.length > 140) { + channel.sendMessage("Sorry, but the message limit is 140 characters <:hiroJey:692008426842226708>") + .queue() + return + } + + if ( + event.message.mentionedMembers.isNotEmpty() || + EMOJI_REGEX.containsMatchIn(text) || + NONASCII_REGEX.containsMatchIn(text) + ) { + channel.sendMessage("Sorry, but I can't display emotes, mentions, or non-latin characters").queue() + return + } + + channel.sendTyping().queue() + + val body = DataObject.empty() + .put("background", background) + .put("character", character) + .put("text", text) + .toString() + .toRequestBody(JSON_TYPE) + + val request = Request.Builder() + .url("https://yuuto.dunctebot.com/dialog") + .post(body) + .build() + + val now = System.currentTimeMillis() + + httpClient.newCall(request).enqueue( + object : OkHttp3Callback { + override fun onFailure(call: Call, e: IOException) { + channel.sendMessage("An error just happened in me, blame the devs <:YoichiLol:701312070880329800>") + .queue() + } + + override fun onResponse(call: Call, response: Response) { + if (response.code != 200) { + val errorMessage = when (response.code) { + 422 -> { + val errorJson = DataObject.fromJson(IOUtil.readFully(IOUtil.getBody(response))) + "There was an error, sorry <:YoichiPlease:692008252690530334> - ${errorJson.getString("message")}" + } + 429 -> "I can't handle this much load at the moment, maybe try again later? <:YoichiPlease:692008252690530334>" + else -> "An error just happened in me, blame the devs <:YoichiLol:701312070880329800>" + } + + channel.sendMessage(errorMessage).queue() + return + } + + val stream = IOUtil.readFully(IOUtil.getBody(response)) + + channel.sendFile(stream, "file.png") + .append(event.author.asMention) + .append(", Here you go!") + .queue() + + logger.info("Generated image for $character at $background, took ${System.currentTimeMillis() - now}ms") + } + } + ) + } + + private fun getBackgroundsAndCharacters(): Pair, MutableList> { + val request = Request.Builder() + .url("https://yuuto.dunctebot.com/info") + .get() + .build() + + httpClient.newCall(request).execute().use { response -> + if (response.code != 200) { + throw Exception("Failed to sync backgrounds and characters with API") + } + val json = jackson.readTree(IOUtil.readFully(IOUtil.getBody(response))) + + return Pair( + jackson.readValue(json["backgrounds"].traverse(), object : TypeReference>() {}), + jackson.readValue(json["characters"].traverse(), object : TypeReference>() {}) + ) + } + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Enlarge.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Enlarge.kt new file mode 100644 index 0000000..80d7aef --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Enlarge.kt @@ -0,0 +1,38 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent + +class Enlarge : AbstractCommand("enlarge", CommandCategory.UTILITIES, "Returns an enlarged emote", "") { + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + val emotes = event.message.emotes + + if (emotes.isEmpty()) { + event.channel.sendMessage("Sorry, but you need to provide me an emote to use this command~!").queue() + return + } + + val emoteLink = emotes.first().imageUrl + + event.channel.sendMessage("${event.author.asMention}, here you go~!\n$emoteLink").queue() + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Help.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Help.kt new file mode 100644 index 0000000..1d09e4d --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Help.kt @@ -0,0 +1,85 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import io.github.yuutoproject.yuutobot.CommandManager +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent + +class Help(private val commandManager: CommandManager) : AbstractCommand( + "help", + CommandCategory.INFO, + "Get the usage of any command", + "[command]", + "Run `help list` to list possible commands" +) { + override val aliases = arrayOf("usage", "commands") + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + val commandName = args.getOrElse(0) { "list" } + + if (commandName == "list") { + val message = event.channel.sendMessage( + "Here is a list of all commands and their descriptions:\n" + ) + + val groups = commandManager.commands.values.groupBy(AbstractCommand::category) + + for ((category, commandsInCategory) in groups) { + message.append("\n$category\n") + + for (command in commandsInCategory) { + message.append("`${command.name}` - ${command.description}\n") + } + } + + message.queue() + return + } + + val command = commandManager.getCommand(commandName) ?: run { + event.channel.sendMessage("Sorry, that command does not exist...").queue() + return + } + + val message = event.channel.sendMessage( + "**Category:** ${command.category}" + ) + + message.append( + if (command.parameters != null) "\n**Usage:** `${commandManager.prefix}${command.name} ${command.parameters}`" + else "\n**Usage:** `${commandManager.prefix}${command.name}`" + ) + + message.append( + "\n**Description:** ${command.description.trim('.', '!')}. " + ) + + if (command.notes != null) { + message.append("${command.notes.trim('.', '!')}.") + } + + if (command.aliases.isNotEmpty()) { + message.append("\n**Aliases:** `${command.aliases.joinToString("`, `")}`") + } + + message.queue() + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Info.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Info.kt new file mode 100644 index 0000000..fe09a4d --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Info.kt @@ -0,0 +1,50 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import io.github.yuutoproject.yuutobot.utils.Constants +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent + +class Info : AbstractCommand("info", CommandCategory.INFO, "Shows the information about the bot and it's developers", "info") { + override val aliases = arrayOf("about", "bot", "credits") + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + val infoEmbed = EmbedBuilder() + .setColor(0xFF93CE) + .setAuthor( + "Yuuto from Camp Buddy", + "https://blitsgames.com", + "https://cdn.discordapp.com/emojis/593518771554091011.png" + ) + .setDescription( + "Yuuto was made and developed by the community, for the community. \n" + + "Join the dev team and start developing on the [project website](https://kyuuto.io/docs). \n\n" + + "Version ${Constants.YUUTO_VERSION} (Kyuuto v${Constants.KYUUTO_VERSION}) was made and developed by: \n" + + "**Arch#0226**, **dunste123#0129**, **zsotroav#8941** \n \n" + + "Quick Change log: \n" + + "```diff\nMoved all code from JavaScript to Kotlin \n```" + ) + .setFooter("Kyuuto | Release ${Constants.KYUUTO_VERSION} - ${Constants.YUUTO_RELEASE}") + + event.channel.sendMessage(infoEmbed.build()).queue() + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Law.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Law.kt new file mode 100644 index 0000000..228bb24 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Law.kt @@ -0,0 +1,44 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent + +class Law : AbstractCommand("law", CommandCategory.INFO, "Shows the buddy law") { + override val aliases = arrayOf("buddylaw") + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + val lawEmbed = EmbedBuilder() + .setColor(0xFF93CE) + .setTitle("The Buddy Law") + .setDescription( + """ + |1) A buddy should be kind, helpful and trustworthy to each other! + |2) A buddy must be always ready for anything! + |3) A buddy should always show a bright smile on his face! + |||4) We leave no buddy behind!|| + """.trimMargin() + ) + + event.channel.sendMessage(lawEmbed.build()).queue() + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Minigame.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Minigame.kt new file mode 100644 index 0000000..de0d17c --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Minigame.kt @@ -0,0 +1,130 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import com.fasterxml.jackson.core.type.TypeReference +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import io.github.yuutoproject.yuutobot.commands.minigame.MinigameInstance +import io.github.yuutoproject.yuutobot.commands.minigame.MinigameListener +import io.github.yuutoproject.yuutobot.objects.Question +import io.github.yuutoproject.yuutobot.utils.jackson +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent +import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionRemoveEvent + +class Minigame : AbstractCommand( + "minigame", + CommandCategory.FUN, + "Play a fun quiz with your friends!", + "Run `minigame` to begin a new game, and react within the countdown to join.\nRun `minigame skip` to skip a question you do not wish to answer." +) { + // String is the channel ID + private var minigames = mutableMapOf() + private val listener = MinigameListener(this) + private val questions: List + + init { + val json = jackson.readTree(this.javaClass.getResource("/minigame.json")) + questions = jackson.readValue(json.traverse(), object : TypeReference>() {}) + } + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + val id = event.channel.idLong + + // Handling for when a game is already in progress + val minigame = minigames[id] + + if (minigame != null) { + // If a user attempts to skip a question + if (args.getOrNull(0) == "skip") { + if (!minigame.players.contains(event.author.idLong)) { + event.channel.sendMessage("You can't skip a question if you aren't in the game!").queue() + return + } + + if (!minigame.begun) { + event.channel.sendMessage("The game has not started yet!").queue() + return + } + + event.channel.sendMessage("Skipping question...").queue() + minigame.progress(event) + return + } + + // If the user attempts to start a new game while a game is already in progress, + // Either cancel it if it's stale or indicate that a game is already in progress + if (System.currentTimeMillis() - minigame.timer > 30_000) { + event.channel.sendMessage("Cancelling stale game...").queue() + unregister(minigame) + // Continue outside of the if block and create a new game instance... + } else { + event.channel.sendMessage("A game is already running!").queue() + return + } + } + + // Register our listener if it doesn't already exist + if (!event.jda.registeredListeners.contains(listener)) { + event.jda.addEventListener(listener) + } + + val maxRounds = args.getOrNull(0)?.toIntOrNull() ?: 7 + if (maxRounds < 2 || maxRounds > 10) { + event.channel.sendMessage("The number of rounds has to be greater than 1 and less than 11.").queue() + return + } + + minigames[id] = MinigameInstance( + questions.shuffled().toMutableList(), + event.channel.idLong, + this, + maxRounds + ) + + // Launch the game instance. + GlobalScope.launch { + minigames[id]!!.start(event) + } + } + + // Used by MinigameInstances to indicate that they're done + fun unregister(minigame: MinigameInstance) { + minigames.remove(minigame.id) + } + + fun messageRecv(event: GuildMessageReceivedEvent) { + val minigame = minigames[event.channel.idLong] ?: return + + if (!minigame.begun || !minigame.players.contains(event.author.idLong)) return + + minigame.answerReceived(event) + } + + fun reactionRecv(event: GuildMessageReactionAddEvent) { + minigames[event.channel.idLong]?.reactionRecv(event) + } + + fun reactionRetr(event: GuildMessageReactionRemoveEvent) { + minigames[event.channel.idLong]?.reactionRetr(event) + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Owoify.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Owoify.kt new file mode 100644 index 0000000..5841c8a --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Owoify.kt @@ -0,0 +1,125 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import io.github.yuutoproject.yuutobot.extensions.applyDefaults +import io.github.yuutoproject.yuutobot.utils.httpClient +import io.github.yuutoproject.yuutobot.utils.jackson +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import net.dv8tion.jda.api.utils.MarkdownSanitizer +import net.dv8tion.jda.internal.utils.IOUtil +import okhttp3.Request +import java.io.File +import java.util.* + +class Owoify : AbstractCommand( + "owoify", + CommandCategory.FUN, + "Yuuto can owoify your text!", + "[level] ", + "[level] can be one of `easy`, `medium` or `hard` to indicate how cringe-worthy the outcome is." +) { + private val levels = listOf("easy", "medium", "hard") + + override val aliases = arrayOf("owo") + + init { + downloadFile() + } + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + if (args.size == 0) { + event.channel.sendMessage("Sorry, but you need to provide me a message to owoify!").queue() + return + } + + val level = if (levels.contains(args[0].toLowerCase())) { + when (args.removeAt(0).toLowerCase()) { + "hard" -> "uvu" + "medium" -> "uwu" + else -> "owo" + } + } else { + "owo" + } + + val input = args.joinToString(" ") + + if (input.length > 1000) { + event.channel.sendMessage("Sorry, but the character limit is 1000~!").queue() + return + } + + val converted = MarkdownSanitizer.escape(runOwo(level, input).replace("`", "\\`")) + + event.channel.sendMessage( + "OwO-ified for ${event.author.asMention}~!\n\n$converted" + ).queue() + } + + private fun runOwo(level: String, message: String): String { + ProcessBuilder("node", FILE_NAME, level, message) + .start() + .inputStream.use { s -> + Scanner(s).use { scanner -> + return buildString { + while (scanner.hasNextLine()) { + appendLine(scanner.nextLine()) + } + } + } + } + } + + private fun downloadFile() { + val file = File(FILE_NAME) + + if (file.exists()) { + logger.debug("File exists") + return + } + + val findRelease = Request.Builder() + .applyDefaults() + .url("https://api.github.com/repos/Yuuto-Project/owo-cli/releases/latest") + .build() + + httpClient.newCall(findRelease).execute().use { res -> + val json = jackson.readTree(res.body!!.byteStream()) + val downLoadUrl = json["assets"][0]["browser_download_url"].asText() + + logger.info("Downloading owoify from $downLoadUrl") + + val download = Request.Builder() + .applyDefaults() + .url(downLoadUrl) + .build() + + httpClient.newCall(download).execute().use { response -> + file.writeBytes(IOUtil.readFully(IOUtil.getBody(response))) + } + } + } + + companion object { + const val FILE_NAME = "owoify.bundle.js" + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Ping.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Ping.kt new file mode 100644 index 0000000..29acaea --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Ping.kt @@ -0,0 +1,42 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent + +class Ping : AbstractCommand( + "ping", + CommandCategory.INFO, + "Get current latency and API ping" +) { + private val pings = arrayOf("Ping", "Pong", "Pang", "Peng") + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + event.jda.restPing.queue { ping -> + // A lot of entities in JDA return a RestAction + // A rest action can do lost of cool things + // But in most cases we just queue it off + // It is important to use queue instead of complete as we don't want to block the event thread + // If we block the event thread JDA cannot receive messages anymore + event.channel.sendMessage("${pings.random()}! Ping is $ping ms").queue() + } + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Route.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Route.kt new file mode 100644 index 0000000..6ac5cb8 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Route.kt @@ -0,0 +1,60 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import com.fasterxml.jackson.core.type.TypeReference +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import io.github.yuutoproject.yuutobot.extensions.getStaticAvatarUrl +import io.github.yuutoproject.yuutobot.objects.Character +import io.github.yuutoproject.yuutobot.utils.jackson +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent + +class Route : AbstractCommand("route", CommandCategory.INFO, "Tells you what route to play next") { + private val endings = listOf("perfect", "good", "bad", "worst") + private val characters: List + + init { + val json = jackson.readTree(this.javaClass.getResource("/routes.json")) + characters = jackson.readValue(json.traverse(), object : TypeReference>() {}) + } + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + val route = characters.random() + val ending = endings.random() + val message = event.message + + val messageEmbed = EmbedBuilder() + .setAuthor(message.member!!.effectiveName, null, message.author.getStaticAvatarUrl()) + .setTitle("Next: ${route.name}, $ending ending") + .setThumbnail(route.emoteId.asEmoteUrl()) + .setDescription(route.description) + .addField("Age", route.age, true) + .addField("Birthday", route.birthday, true) + .addField("Animal Motif", route.animal, true) + .setFooter("Play ${route.firstName}'s route next. All bois are best bois.") + .setColor(route.color) + .build() + + event.channel.sendMessage(messageEmbed).queue() + } + + private fun String.asEmoteUrl() = "https://cdn.discordapp.com/emojis/$this.gif?v=1" +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Ship.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Ship.kt new file mode 100644 index 0000000..bc217b4 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/Ship.kt @@ -0,0 +1,209 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands + +import io.github.yuutoproject.yuutobot.commands.base.AbstractCommand +import io.github.yuutoproject.yuutobot.commands.base.CommandCategory +import io.github.yuutoproject.yuutobot.extensions.JSON_TYPE +import io.github.yuutoproject.yuutobot.extensions.applyDefaults +import io.github.yuutoproject.yuutobot.extensions.getStaticAvatarUrl +import io.github.yuutoproject.yuutobot.utils.findMember +import io.github.yuutoproject.yuutobot.utils.httpClient +import io.github.yuutoproject.yuutobot.utils.jackson +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.entities.Member +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import net.dv8tion.jda.api.utils.data.DataObject +import net.dv8tion.jda.internal.utils.IOUtil +import okhttp3.Call +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import java.io.File +import java.io.IOException +import okhttp3.Callback as OkHttp3Callback + +class Ship : AbstractCommand( + "ship", + CommandCategory.FUN, + "Yuuto mastered the art of shipping users and can now calculate if you and your crush will work out", + " " +) { + private val shipMessages: Map + private val riggedUsers: Map + private val selfShipMessages = listOf( + "Unsurprising, loving yourself is all you do around here.", + "Who would've thought that you'd love yourself this much" + ) + + init { + // mutable map keeps the order, a hashmap does not + val messagesMap = mutableMapOf() + val json = jackson.readTree(this.javaClass.getResource("/ship_messages.json5")) + + json.forEach { + messagesMap[it.get("max_score").asInt()] = it.get("message").asText() + } + + // turn the map into a read only map + this.shipMessages = messagesMap.toMap() + this.riggedUsers = this.loadRiggedShips() + } + + override fun run(args: MutableList, event: GuildMessageReceivedEvent) { + val channel = event.channel + + if (args.size < 2) { + channel.sendMessage("This command requires two arguments: `ship `.").queue() + return + } + + val name1 = args[0] + val member1 = findMember(name1, event) + + if (member1 == null) { + channel.sendMessage("No user found for input $name1").queue() + return + } + + val name2 = args[1] + val member2 = findMember(name2, event) + + if (member2 == null) { + channel.sendMessage("No user found for input $name2").queue() + return + } + + val (score, message) = this.getScoreAndMessage(member1, member2) + val member1Avatar = member1.getStaticAvatarUrl() + val member2Avatar = member2.getStaticAvatarUrl() + val imageUrl = "https://api.alexflipnote.dev/ship?user=$member1Avatar&user2=$member2Avatar" + + val parsedMessage = if (message.contains("%s")) { + message.format(member1.effectiveName, member2.effectiveName) + } else { + message + } + + fetchShipImage(member1Avatar, member2Avatar) { + val embed = EmbedBuilder() + .setTitle("${member1.effectiveName} and ${member2.effectiveName}") + .addField("Your love score is $score%", parsedMessage, false) + .setImage("attachment://ship.png") + .build() + + channel.sendFile(it, "ship.png") + .embed(embed) + .queue() + } + } + + private fun shouldBeRigged(member1: Member, member2: Member): Boolean { + val id1 = member1.idLong + val id2 = member2.idLong + + return this.riggedUsers[id1] == id2 || this.riggedUsers[id2] == id1 + } + + private fun calculateScore(member1: Member, member2: Member): Int { + val score = (member1.idLong + member2.idLong) / 7 + + // convert the long to an int + return (score % 100).toInt() + } + + private fun getMessageFromScore(score: Int): String { + val scoreKey = this.shipMessages.keys.first { score <= it } + + return this.shipMessages[scoreKey] ?: error("Excuse me but how is this even possible?") + } + + private fun getScoreAndMessage(member1: Member, member2: Member): Pair { + if (member1 == member2) { + return 100 to selfShipMessages.random() + } + + if (this.shouldBeRigged(member1, member2)) { + // We're using the getMessageFromScore method here so it uses the message from the json + // this way we only have to change the message in one place when we update it + return 100 to this.getMessageFromScore(100) + } + + val score = this.calculateScore(member1, member2) + val message = this.getMessageFromScore(score) + + return score to message + } + + private fun fetchShipImage(image1: String, image2: String, callback: (ByteArray) -> Unit) { + val body = DataObject.empty() + .put("image1", image1) + .put("image2", image2) + .toString() + .toRequestBody(JSON_TYPE) + + val request = Request.Builder() + .applyDefaults() + .post(body) + .url("https://apis.duncte123.me/images/love") + .build() + + httpClient.newCall(request).enqueue( + object : OkHttp3Callback { + override fun onFailure(call: Call, e: IOException) { + e.printStackTrace() // the ugly way + } + + override fun onResponse(call: Call, response: Response) { + response.use { + val bytes = IOUtil.readFully(IOUtil.getBody(it)) + + callback(bytes) + } + } + } + ) + } + + /** + * Loads in all of the rigged ships from the json file stored IN THE ROOT OF THE CURRENT WORKING DIRECTORY + * The format of the file is + * { "user id": "user id" } + * The order of the ids doesn't matter as the ap checks both ways + */ + private fun loadRiggedShips(): Map { + val shipsFile = File("rigged_ships.json5") + + if (!shipsFile.exists()) { + logger.warn("Skipping rigged ships as file does not exist") + return mapOf() + } + + val riggedMap = hashMapOf() + val riggedJson = jackson.readTree(shipsFile) + + riggedJson.fieldNames().forEach { + riggedMap[it.toLong()] = riggedJson.get(it).asLong() + } + + logger.info("Loaded rigged ships $riggedMap") + + return riggedMap.toMap() + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/base/AbstractCommand.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/base/AbstractCommand.kt new file mode 100644 index 0000000..ae21294 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/base/AbstractCommand.kt @@ -0,0 +1,55 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands.base + +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import org.slf4j.LoggerFactory + +/** + * Base class for all commands + * + * The following properties have to be filled + * - [name]: The name of the command + * - [category]: The category of the command + * - [description]: Oneliner that shows in "y!help list" + * - [parameters]: Short string of all the parameters the command accepts + * - [notes]: Additional notes that the user may have to note + * + * Commands can add aliases by overriding the [aliases] prop + */ +abstract class AbstractCommand( + val name: String, + val category: CommandCategory, + val description: String, + val parameters: String? = null, + val notes: String? = null +) { + protected val logger = LoggerFactory.getLogger(this.javaClass) + + /** + * List of aliases for the command, is empty by default + */ + open val aliases = emptyArray() + + abstract fun run(args: MutableList, event: GuildMessageReceivedEvent) + + override fun toString(): String { + return "AbstractCommand(name='$name', category=$category, description='$description', usage='$notes')" + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/base/CommandCategory.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/base/CommandCategory.kt new file mode 100644 index 0000000..a546d65 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/base/CommandCategory.kt @@ -0,0 +1,30 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands.base + +enum class CommandCategory(private val emote: String) { + INFO("\u2139"), + FUN("\uD83C\uDFB2"), + UTILITIES("\u2699"); + + // This is an instance prop + private val displayName: String = name.toLowerCase().capitalize() + + override fun toString() = "$emote $displayName" +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/minigame/MinigameInstance.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/minigame/MinigameInstance.kt new file mode 100644 index 0000000..1f4bf9e --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/minigame/MinigameInstance.kt @@ -0,0 +1,186 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands.minigame + +import io.github.yuutoproject.yuutobot.commands.Minigame +import io.github.yuutoproject.yuutobot.objects.Question +import kotlinx.coroutines.delay +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent +import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionRemoveEvent + +class MinigameInstance( + private val questions: MutableList, + // The ID of the channel the game is in + // Effectively the ID of the game instance itself + val id: Long, + // This is necessary for when the game is finished and we want to remove the object + private val manager: Minigame, + private val maxRounds: Int +) { + // Is the game in progress or not? + var begun = false + + // String ID of user and their scores + var players = mutableMapOf() + var timer = System.currentTimeMillis() + + private var rounds = 1 + + private lateinit var startingMessageID: String + + private lateinit var currentQuestion: Question + private lateinit var currentAnswers: MutableList + + suspend fun start(event: GuildMessageReceivedEvent) { + event.channel.sendMessage("Starting a game with $maxRounds rounds...").queue() + + val embed = EmbedBuilder() + .setColor(0xFF93CE) + .setTitle("Minigame Starting!") + .setDescription( + "React below to join the game! \n" + + "This game may contain spoilers or NSFW themes.\n" + + "Please run `minigame skip` in order to skip a question." + ) + val startingMessage = event.channel.sendMessage(embed.build()).complete() + startingMessageID = startingMessage.id + + startingMessage.addReaction("\uD83C\uDDF4").complete() + + for (countdown in 10 downTo 0 step 2) { + embed.setDescription( + "React below to join the game!\n" + + "This game may contain spoilers or NSFW themes.\n" + + "Please run `minigame skip` in order to skip a question.\n" + + "Current players: ${getPlayers()}\n" + + "$countdown seconds left!" + ) + startingMessage.editMessage(embed.build()).complete() + + delay(2000) + } + + if (players.isEmpty()) { + embed.setTitle("Minigame cancelled!").setDescription("Nobody joined...") + startingMessage.editMessage(embed.build()).complete() + manager.unregister(this) + return + } + + embed.setTitle("Minigame started!").setDescription("The game has begun!") + startingMessage.editMessage(embed.build()).complete() + + begun = true + progress(event) + } + + fun progress(event: GuildMessageReceivedEvent) { + if (rounds > maxRounds) { + endGame(event) + return + } + + // Unfortunately, no removeOrNull, so we have to use a try/catch + try { + currentQuestion = questions.removeAt(0) + } catch (e: IndexOutOfBoundsException) { + endGame(event) + return + } + + currentAnswers = currentQuestion.answers.map { it.toLowerCase() } + .toMutableList() + + if (currentQuestion.type == "FILL") { + event.channel.sendMessage(currentQuestion.question).queue() + } else if (currentQuestion.type == "MULTIPLE") { + val questionString = "${currentQuestion.question}\n" + + val answerString = (currentQuestion.wrong + currentQuestion.answers).shuffled() + .mapIndexed { i, answer -> + if (currentAnswers.contains(answer.toLowerCase())) { + currentAnswers.add((i + 1).toString()) + } + + "${i + 1}) $answer" + }.joinToString("\n") + + event.channel.sendMessage(questionString + answerString).queue() + } + } + + private fun endGame(event: GuildMessageReceivedEvent) { + // Sort by descending value and then map each value to a line in the scoreboard, then join it + val scoreboard = players.entries.sortedByDescending { it.value }.mapIndexed { i, entry -> + "${i + 1}) <@${entry.key}> with ${entry.value} points" + }.joinToString("\n") + + val embed = EmbedBuilder() + .setColor(0xFF93CE) + .setTitle("Minigame ended!") + .setDescription("Total points:\n$scoreboard") + event.channel.sendMessage(embed.build()).queue() + + manager.unregister(this) + } + + fun answerReceived(event: GuildMessageReceivedEvent) { + // A new guess is made, so we reset the stale-game timer + timer = System.currentTimeMillis() + + if (currentAnswers.contains(event.message.contentStripped.toLowerCase())) { + players[event.author.idLong] = players[event.author.idLong]!! + 1 + event.channel.sendMessage("<@${event.author.id}> got the point!").queue() + rounds += 1 + progress(event) + } + } + + fun reactionRecv(event: GuildMessageReactionAddEvent) { + if ( + event.user.isBot || + players.contains(event.user.idLong) || + begun || + event.messageId != startingMessageID || + !event.reactionEmote.isEmoji || + event.reactionEmote.emoji != "\uD83C\uDDF4" + ) return + + players[event.user.idLong] = 0 + } + + fun reactionRetr(event: GuildMessageReactionRemoveEvent) { + if ( + event.messageId == startingMessageID && + event.reactionEmote.isEmoji && + event.reactionEmote.emoji == "\uD83C\uDDF4" && + !begun + ) { + players.remove(event.user!!.idLong) + } + } + + private fun getPlayers() = if (players.isNotEmpty()) { + players.keys.joinToString(", ") { "<@$it>" } + } else { + "none" + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/minigame/MinigameListener.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/minigame/MinigameListener.kt new file mode 100644 index 0000000..41f7cd8 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/commands/minigame/MinigameListener.kt @@ -0,0 +1,36 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.commands.minigame + +import io.github.yuutoproject.yuutobot.commands.Minigame +import net.dv8tion.jda.api.events.GenericEvent +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent +import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionRemoveEvent +import net.dv8tion.jda.api.hooks.EventListener + +class MinigameListener(private val minigame: Minigame) : EventListener { + override fun onEvent(event: GenericEvent) { + when (event) { + is GuildMessageReceivedEvent -> minigame.messageRecv(event) + is GuildMessageReactionAddEvent -> minigame.reactionRecv(event) + is GuildMessageReactionRemoveEvent -> minigame.reactionRetr(event) + } + } +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/extensions/Member.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/extensions/Member.kt new file mode 100644 index 0000000..968eb27 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/extensions/Member.kt @@ -0,0 +1,35 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.extensions + +import io.github.yuutoproject.yuutobot.utils.Constants.DEV_ROLE_ID +import io.github.yuutoproject.yuutobot.utils.Constants.KYUUTO_GUILD +import net.dv8tion.jda.api.entities.Member + +fun Member.getStaticAvatarUrl(size: Int = 128): String { + return this.user.getStaticAvatarUrl(size) +} + +val Member.isDeveloper: Boolean + get() { + val guild = this.jda.getGuildById(KYUUTO_GUILD) ?: return false + val member = guild.getMemberById(this.idLong) ?: return false + + return member.roles.contains(guild.getRoleById(DEV_ROLE_ID)!!) + } diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/extensions/Okhttp.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/extensions/Okhttp.kt new file mode 100644 index 0000000..5c4cea1 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/extensions/Okhttp.kt @@ -0,0 +1,35 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.extensions + +import io.github.yuutoproject.yuutobot.utils.Constants +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Request + +val JSON_TYPE = "application/json; charset=utf-8".toMediaType() + +fun Request.Builder.applyDefaults(): Request.Builder { + this.header( + "User-Agent", + "Yuuto Discord Bot / ${Constants.YUUTO_VERSION} https://github.com/Yuuto-Project/kyuuto" + ) + this.get() + + return this +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/extensions/User.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/extensions/User.kt new file mode 100644 index 0000000..581c2cc --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/extensions/User.kt @@ -0,0 +1,31 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.extensions + +import net.dv8tion.jda.api.entities.User + +fun User.getStaticAvatarUrl(size: Int = 128): String { + val url = if (this.avatarId == null) { + this.defaultAvatarUrl + } else { + User.AVATAR_URL.format(this.idLong, this.avatarId, "png") + } + + return "$url?size=$size" +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/objects/Character.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/objects/Character.kt new file mode 100644 index 0000000..19e29b4 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/objects/Character.kt @@ -0,0 +1,36 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.objects + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import io.github.yuutoproject.yuutobot.Utils + +class Character @JsonCreator constructor( + @JsonProperty("name") val name: String, + @JsonProperty("description") val description: String, + @JsonProperty("age") val age: String, + @JsonProperty("birthday") val birthday: String, + @JsonProperty("animal") val animal: String, + @JsonProperty("color") private val colorString: String, + @JsonProperty("emoteId") val emoteId: String +) { + val color = Utils.hexStringToInt(colorString) + val firstName = name.split(" ")[0] +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/objects/Question.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/objects/Question.kt new file mode 100644 index 0000000..f95c6b7 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/objects/Question.kt @@ -0,0 +1,29 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.objects + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +class Question @JsonCreator constructor( + @JsonProperty("type") val type: String, + @JsonProperty("question") val question: String, + @JsonProperty("answers") val answers: MutableList, + @JsonProperty("wrong") val wrong: List +) diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/Constants.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/Constants.kt new file mode 100644 index 0000000..cf23cf3 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/Constants.kt @@ -0,0 +1,30 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.utils + +// This class should only contain static booleans, ints, strings etc (no classes) +// as they are hardcoded into the code at compile time +object Constants { + const val YUUTO_RELEASE = "2020-07-18" // Release date of the current version + const val YUUTO_VERSION = "3.0" // Version of the bot (the user sees) + const val KYUUTO_VERSION = "1.0" // Version of Kyuuto, the Kotlin version + + const val KYUUTO_GUILD = 684392803131850779L + const val DEV_ROLE_ID = 691420289342505102L +} diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/Container.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/Container.kt new file mode 100644 index 0000000..b6eda74 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/Container.kt @@ -0,0 +1,35 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.utils + +import com.fasterxml.jackson.core.json.JsonReadFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import okhttp3.OkHttpClient + +val httpClient = OkHttpClient() + +// Special jackson configuration to allow for json and json5 (partially) loading +val jackson = JsonMapper.builder() + .enable( + JsonReadFeature.ALLOW_TRAILING_COMMA, + JsonReadFeature.ALLOW_JAVA_COMMENTS, + JsonReadFeature.ALLOW_YAML_COMMENTS, + JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES + ) + .build() diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/discordPatterns.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/discordPatterns.kt new file mode 100644 index 0000000..bf45c65 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/discordPatterns.kt @@ -0,0 +1,22 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.utils + +val EMOJI_REGEX = "(?:[\\u2700-\\u27bf]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff]|[\\u0023-\\u0039]\\ufe0f?\\u20e3|\\u3299|\\u3297|\\u303d|\\u3030|\\u24c2|\\ud83c[\\udd70-\\udd71]|\\ud83c[\\udd7e-\\udd7f]|\\ud83c\\udd8e|\\ud83c[\\udd91-\\udd9a]|\\ud83c[\\udde6-\\uddff]|\\ud83c[\\ude01-\\ude02]|\\ud83c\\ude1a|\\ud83c\\ude2f|\\ud83c[\\ude32-\\ude3a]|\\ud83c[\\ude50-\\ude51]|\\u203c|\\u2049|[\\u25aa-\\u25ab]|\\u25b6|\\u25c0|[\\u25fb-\\u25fe]|\\u00a9|\\u00ae|\\u2122|\\u2139|\\ud83c\\udc04|[\\u2600-\\u26FF]|\\u2b05|\\u2b06|\\u2b07|\\u2b1b|\\u2b1c|\\u2b50|\\u2b55|\\u231a|\\u231b|\\u2328|\\u23cf|[\\u23e9-\\u23f3]|[\\u23f8-\\u23fa]|\\ud83c\\udccf|\\u2934|\\u2935|[\\u2190-\\u21ff])/g".toRegex() +val NONASCII_REGEX = "[^\\x00-\\x7F]".toRegex() diff --git a/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/memberUtils.kt b/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/memberUtils.kt new file mode 100644 index 0000000..1dc3157 --- /dev/null +++ b/src/main/kotlin/io/github/yuutoproject/yuutobot/utils/memberUtils.kt @@ -0,0 +1,35 @@ +/* + * Open source bot built by and for the Camp Buddy Discord Fan Server. + * Copyright (C) 2020 Kyuuto-devs + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package io.github.yuutoproject.yuutobot.utils + +import com.jagrosh.jdautilities.commons.utils.FinderUtil +import net.dv8tion.jda.api.entities.Member +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent + +fun findMember(input: String, event: GuildMessageReceivedEvent): Member? { + val foundMembers = FinderUtil.findMembers(input, event.guild) + + if (foundMembers.isEmpty()) { + return null + } + + // why not "?: null" + // Well java is fun and will throw an index out of bounds exception if there is no first element + return foundMembers[0] +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..5674c0b --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + + + + %d{HH:mm:ss.SSS} %boldCyan(%-34.-34thread) %red(%10.10X{jda.shard}) %boldGreen(%-15.-15logger{0}) %highlight(%-6level) %msg%n + + + + + + + + diff --git a/src/main/resources/minigame.json b/src/main/resources/minigame.json new file mode 100644 index 0000000..7218b0e --- /dev/null +++ b/src/main/resources/minigame.json @@ -0,0 +1,265 @@ +[ + { + "type": "FILL", + "question": "Who is the owner of Camp Buddy?", + "answers": [ + "Goro" + ], + "wrong": [] + }, + { + "type": "FILL", + "question": "What is the animal motif of Keitaro?", + "answers": [ + "frog", + "frogs" + ], + "wrong": [] + }, + { + "type": "FILL", + "question": "What is the animal motif of Hiro?", + "answers": [ + "raccoon", + "raccoons" + ], + "wrong": [] + }, + { + "type": "FILL", + "question": "What is the animal motif of Hunter?", + "answers": [ + "bunny", + "bunnies" + ], + "wrong": [] + }, + { + "type": "FILL", + "question": "What is the animal motif of Natsumi?", + "answers": [ + "beetle", + "beetles" + ], + "wrong": [] + }, + { + "type": "FILL", + "question": "What is the animal motif of Yoichi?", + "answers": [ + "wolf", + "wolves" + ], + "wrong": [] + }, + { + "type": "FILL", + "question": "What is the animal motif of Taiga?", + "answers": [ + "tiger", + "tigers" + ], + "wrong": [] + }, + { + "type": "MULTIPLE", + "question": "Who is the oldest camper?", + "answers": [ + "Natsumi" + ], + "wrong": [ + "Yoichi", + "Hiro", + "Hunter" + ] + }, + { + "type": "MULTIPLE", + "question": "What nickname did Yoichi give Natsumi?", + "answers": [ + "Mr. Perfect" + ], + "wrong": [ + "Mr. Incredible", + "Mr. Excellency", + "Mr. Wonderful" + ] + }, + { + "type": "MULTIPLE", + "question": "What did Natsumi and Keitaro see in Natsumi's perfect ending?", + "answers": [ + "Fireworks" + ], + "wrong": [ + "Game show", + "Concert", + "Opera" + ] + }, + { + "type": "MULTIPLE", + "question": "What is the real reason Hiro brought Keitaro to the camp?", + "answers": [ + "To say goodbye" + ], + "wrong": [ + "To have the greatest memories", + "To become best friends", + "To have fun" + ] + }, + { + "type": "FILL", + "question": "Who drove Keitaro and Hiro to the hospital?", + "answers": [ + "Yoshi", + "Yoshinori" + ], + "wrong": [] + }, + { + "type": "MULTIPLE", + "question": "What did Hiro and Keitaro do during Nature's Day?", + "answers": [ + "Planting seeds" + ], + "wrong": [ + "Drawing pictures", + "Cooking with Aiden", + "Helping out" + ] + }, + { + "type": "MULTIPLE", + "question": "In Hunter's perfect ending, Keitaro was helping Hunter in his?", + "answers": [ + "Dormitory" + ], + "wrong": [ + "Headquarter", + "Villa", + "Mansion" + ] + }, + { + "type": "MULTIPLE", + "question": "The final design of the poster for the fundraiser is?", + "answers": [ + "A campfire" + ], + "wrong": [ + "A tent", + "A cabin", + "A waterfall" + ] + }, + { + "type": "FILL", + "question": "What did Yoshinori gift to Keitaro halfway through Taiga's route?", + "answers": [ + "bracelet", + "friendship bracelet" + ], + "wrong": [] + }, + { + "type": "MULTIPLE", + "question": "While renovating the cabin, what was taiga depicted as on the wall?", + "answers": [ + "Devil" + ], + "wrong": [ + "Snake", + "Woman", + "Mosquito" + ] + }, + { + "type": "MULTIPLE", + "question": "When did Keitaro lose the Sportfest?", + "answers": [ + "On Taiga's route" + ], + "wrong": [ + "On bad ending", + "When you chose wrongly" + ] + }, + { + "type": "MULTIPLE", + "question": "Who is the first person Keitaro sees upon reaching Camp Buddy?", + "answers": [ + "Hiro" + ], + "wrong": [ + "Yoichi", + "Natsumi", + "Hunter" + ] + }, + { + "type": "MULTIPLE", + "question": "Which one of these isn't one of the official Buddy Laws?", + "answers": [ + "We leave no buddy behind!" + ], + "wrong": [ + "A buddy should be kind, helpful and trustworthy to each other!", + "A buddy must be always ready for anything!", + "A buddy should always show a bright smile on his face!" + ] + }, + { + "type": "MULTIPLE", + "question": "What is the name of the Camp Buddy Theme song?", + "answers": [ + "Greatest Memories" + ], + "wrong": [ + "Camping Time!", + "Old friend", + "Sweet sorrow" + ] + }, + { + "type": "MULTIPLE", + "question": "Who did Yoichi bully on the first day?", + "answers": [ + "Hunter" + ], + "wrong": [ + "Hiro", + "Taiga", + "Felix" + ] + }, + { + "type": "FILL", + "question": "Fill in the missing part: Camp Buddy is a XXXXX-themed Summer Camp.", + "answers": [ + "scout" + ], + "wrong": [] + }, + { + "type": "FILL", + "question": "How many different endings are possible in **each route**?", + "answers": [ + "4" + ], + "wrong": [] + }, + { + "type": "MULTIPLE", + "question": "What did Aiden spill onto Yoshi in the first day?", + "answers": [ + "milk" + ], + "wrong": [ + "soup", + "tea", + "water" + ] + } +] diff --git a/src/main/resources/routes.json b/src/main/resources/routes.json new file mode 100644 index 0000000..e18afe5 --- /dev/null +++ b/src/main/resources/routes.json @@ -0,0 +1,56 @@ +[ + { + "name": "Hiro Akiba", + "description": "As Keitaro's childhood friend and best friend, Hiro has growing affection for Keitaro. He tends to get jealous easily, but his cheerful personality and talent for cooking make up for it!", + "age": 19, + "birthday": "September 22", + "animal": "Raccoon", + "color": "#ff6600", + "emoteId": "514541939979452431" + }, + { + "name": "Keitaro Nagame", + "description": "Keitaro is an energetic guy who loves to 'collect' memories through taking pictures. His playful and friendly nature makes him a natural crowdpleaser.", + "age": 19, + "birthday": "April 8", + "animal": "Frog", + "color": "#42a82b", + "emoteId": "514627809919107072" + }, + { + "name": "Hunter Springfield", + "description": "Hunter has a shy and timid personality. He is always having a hard time socializing with others. It may look like that he prefers spending his time alone, but he just wants to have friends, just like everybody else.", + "age": 18, + "birthday": "March 20", + "animal": "Bunny", + "color": "#f7f63c", + "emoteId": "514543742167023625" + }, + { + "name": "Natsumi Hamasaki", + "description": "Natsumi is seen as a role model among the campers due to his responsible and hardworking nature. However, he is pressured with the high expectations built around him.", + "age": 20, + "birthday": "June 20", + "animal": "Stag Beetle", + "color": "#1ab1ff", + "emoteId": "514542811937505301" + }, + { + "name": "Yoichi Yukimura", + "description": "Yoichi heeds no authority and likes to bully others, giving him the reputation of a delinquent. No one understands the true reason behind his actions. Surprisingly, he has a soft heart when it comes to animals.", + "age": 20, + "birthday": "January 21", + "animal": "Wolf", + "color": "#b11aff", + "emoteId": "514543014979567617" + }, + { + "name": "Taiga Akatora", + "description": "Taiga is a born leader, but unfortunately, he's using it to drag his friends into his schemes. He has an unreasonable hate towards Keitaro.", + "age": 19, + "birthday": "July 13", + "animal": "Tiger", + "color": "#e81615", + "emoteId": "514535865691930634" + } +] diff --git a/src/main/resources/ship_messages.json5 b/src/main/resources/ship_messages.json5 new file mode 100644 index 0000000..d3c02ee --- /dev/null +++ b/src/main/resources/ship_messages.json5 @@ -0,0 +1,43 @@ +[ + { + max_score: 10, + message: "This will probably be another Camp Disaster, don't bother with it.", + }, + { + max_score: 29, + message: "Things will work out great between you guys, if you two stay in separate cabins.", + }, + { + max_score: 39, + message: "This will work out just as well as Yuri and Yoshinori.", + }, + { + // Important: Yuuto is still pessimistic here + max_score: 49, + message: "My magic beach ball says that you shouldn't count on it.", + }, + { + max_score: 64, + message: "This might work, it might also not work. All you can do is try.", + }, + { + max_score: 68, + message: "Breathe in and drink some water, you two might have a future.", + }, + { + max_score: 69, + message: "Nice", + }, + { + max_score: 89, + message: "Hey! You two should kiss!", // Guess the reference :P + }, + { + max_score: 99, + message: "Wah this is amazingly rare! Stop whatever you're doing and go make out, NOW!", + }, + { + max_score: 100, + message: "%s and %s are like me and volleyball, a perfect match!", + }, +]