├── .gitignore ├── LICENSE.md ├── README.md ├── _config.yml ├── build.gradle.kts ├── commands.md ├── docker └── Dockerfile ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── guide.md ├── setup.md └── src └── main ├── kotlin └── me │ └── jakejmattson │ └── embedbot │ ├── MainApp.kt │ ├── arguments │ ├── EmbedArg.kt │ ├── FieldArg.kt │ ├── FieldIndexArg.kt │ └── GroupArg.kt │ ├── commands │ ├── CoreCommands.kt │ ├── EditCommands.kt │ ├── FieldCommands.kt │ ├── GroupCommands.kt │ ├── GuildConfigurationCommands.kt │ ├── InfoCommands.kt │ ├── OwnerCommands.kt │ └── UtilityCommands.kt │ ├── dataclasses │ ├── Cluster.kt │ ├── Configuration.kt │ ├── Embed.kt │ ├── GuildEmbeds.kt │ └── OperationResult.kt │ ├── extensions │ ├── Command.kt │ ├── Guild.kt │ └── Message.kt │ ├── listeners │ └── SetupListeners.kt │ ├── preconditions │ ├── LoadedEmbedPrecondition.kt │ └── PermissionPrecondition.kt │ ├── services │ ├── EmbedService.kt │ └── PermissionsService.kt │ └── utils │ └── Constants.kt └── resources └── templates └── readme-template.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Configuration 2 | config/ 3 | 4 | # IntelliJ 5 | .idea/ 6 | *.iml 7 | 8 | # Gradle 9 | .gradle/ 10 | build/ 11 | gradle.properties -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jacob Mattson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Kotlin 1.4.0 4 | 5 | 6 | DiscordKt 0.19.1 7 |
8 | 9 | Release 10 | 11 | 12 | Docker 13 | 14 |
15 | 16 | Discord JakeyWakey#1569 17 | 18 |

19 | 20 | # EmbedBot 21 | EmbedBot is a simple bot interface to create and modify embeds via commands. Creating embeds is not possible with a user account via the traditional Discord interface. You can create and store as many embeds as you want. These can be imported and exported as JSON for easy sharing between servers or persistent storage. You can also group embeds together, allowing you to deploy and manage multiple embeds at once. This will allow servers to easily set up a rules channel, keeping all of the related embeds grouped together and ready to send with a single command. 22 | 23 | ## Add it 24 | You can either [invite](https://discordapp.com/oauth2/authorize?client_id=439163847618592782&scope=bot&permissions=101440) it to your server or host it yourself with the [setup guide](setup.md). 25 | 26 | ## Using it 27 | To use the bot, see the [User Guide](guide.md) or a full list of [commands](commands.md). -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-modernist -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | group = "me.jakejmattson" 4 | version = Versions.BOT 5 | 6 | plugins { 7 | kotlin("jvm") version "1.4.0" 8 | id("com.github.johnrengelman.shadow") version "6.0.0" 9 | } 10 | 11 | repositories { 12 | mavenCentral() 13 | jcenter() 14 | } 15 | 16 | dependencies { 17 | implementation("me.jakejmattson:DiscordKt:${Versions.DISCORDKT}") 18 | } 19 | 20 | tasks { 21 | withType { 22 | kotlinOptions.jvmTarget = "1.8" 23 | } 24 | 25 | copy { 26 | from(file("src/main/resources/templates/readme-template.md")) 27 | into(file(".")) 28 | rename{ "README.md" } 29 | expand( 30 | "kotlin" to kotlin.coreLibrariesVersion, 31 | "discordkt" to Versions.DISCORDKT 32 | ) 33 | } 34 | 35 | shadowJar { 36 | archiveFileName.set("EmbedBot.jar") 37 | manifest { 38 | attributes( 39 | "Main-Class" to "me.jakejmattson.embedbot.MainAppKt" 40 | ) 41 | } 42 | } 43 | } 44 | 45 | object Versions { 46 | const val BOT = "2.1.0" 47 | const val DISCORDKT = "0.19.1" 48 | } -------------------------------------------------------------------------------- /commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | ## Key 4 | | Symbol | Meaning | 5 | | ----------- | ------------------------------ | 6 | | (Argument) | Argument is not required. | 7 | | Argument... | Accepts many of this argument. | 8 | 9 | ## Core 10 | | Commands | Arguments | Description | 11 | | ------------ | ------------------------------ | ------------------------------------------------------- | 12 | | Copy | Embed Name, Message Link or ID | Copy an embed by its message ID. | 13 | | Create | Embed Name | Create a new embed with this name. | 14 | | Delete | (Embed...) | Delete the embed with this name. | 15 | | Duplicate | Embed Name, (Embed) | Create a new embed from an existing embed. | 16 | | ExecuteAll | Commands | Execute a batch of commands in sequence. | 17 | | Export | (Embed), (preferFile) | Export the currently loaded embed to JSON. | 18 | | Import | Embed Name, File \| String | Import a JSON file or string as an embed. | 19 | | Load | Embed | Load the embed with this name into memory. | 20 | | Send | (Channel), (saveLocation) | Send the currently loaded embed. | 21 | | Update | | Update the message embed | 22 | | UpdateTarget | Message Link or ID | Replace the target message embed with the loaded embed. | 23 | 24 | ## Edit 25 | | Commands | Arguments | Description | 26 | | -------------- | ----------------- | --------------------------------------------------- | 27 | | Clear | (Clear Target) | Clear a target field from the loaded embed. | 28 | | Rename | (Embed), New Name | Change the name of an existing embed. | 29 | | SetAuthor | User | Set the author for the currently loaded embed. | 30 | | SetColor | Hex Color | Set the color for the currently loaded embed. | 31 | | SetDescription | Text | Set the description for the currently loaded embed. | 32 | | SetFooter | Icon URL, Text | Set the footer for the currently loaded embed. | 33 | | SetImage | URL | Set the image for the currently loaded embed. | 34 | | SetThumbnail | URL | Set the thumbnail for the currently loaded embed. | 35 | | SetTimestamp | | Set the timestamp for the currently loaded embed. | 36 | | SetTitle | Text | Set the title for the currently loaded embed. | 37 | 38 | ## Field 39 | | Commands | Arguments | Description | 40 | | -------------- | ----------------------- | -------------------------------------------------------- | 41 | | AddBlankField | (isInline) | Add a blank field to the loaded embed. | 42 | | AddField | Field Data | Add a field in the following format: title\|body\|inline | 43 | | InsertField | Index, Field Data | Insert a field at an index to the loaded embed. | 44 | | RemoveField | Field Index | Remove a field from the loaded embed by its index. | 45 | | SetField | Field Index, Field Data | Edit a field at a given index with the given data. | 46 | | SetFieldInline | Field Index, Boolean | Get a field by its index and edit its inline value. | 47 | | SetFieldText | Field Index, Text | Get a field by its index and edit its text value. | 48 | | SetFieldTitle | Field Index, Text | Get a field by its index and edit its title value. | 49 | 50 | ## Group 51 | | Commands | Arguments | Description | 52 | | --------------- | ------------------------------- | ------------------------------------------------------ | 53 | | AddToGroup | Group, Embed... | Add an embed into a group. | 54 | | CloneGroup | Group Name, (Channel), Amount | Clone a group of embeds. | 55 | | CreateGroup | Group Name, (Embed...) | Create a group of embeds. | 56 | | DeleteGroup | Group | Delete a group and all of its embeds. | 57 | | Deploy | Group, (Channel), (saveLocation) | Deploy a group into a target channel. | 58 | | InsertIntoGroup | Group, Index, Embed | Insert an embed into a group at an index. | 59 | | RemoveFromGroup | Embed... | Remove embeds from their current group. | 60 | | RenameGroup | Group, New Name | Change the name of an existing group. | 61 | | UpdateGroup | Group | Update the original embeds this group was copied from. | 62 | 63 | ## GuildConfiguration 64 | | Commands | Arguments | Description | 65 | | --------- | --------- | ---------------------------------------------------------- | 66 | | DeleteAll | | Delete all embeds and groups in this guild. | 67 | | SetPrefix | Prefix | Set the prefix required for the bot to register a command. | 68 | | SetRole | Role | Set the role required to use this bot. | 69 | 70 | ## Information 71 | | Commands | Arguments | Description | 72 | | ---------- | --------- | --------------------------------------- | 73 | | Info | (Embed) | Get extended info for the target embed. | 74 | | Limits | | Display the discord embed limits. | 75 | | ListEmbeds | | List all embeds created in this guild. | 76 | 77 | ## Owner 78 | | Commands | Arguments | Description | 79 | | --------- | --------------------------------- | ------------------------------------------------------- | 80 | | Broadcast | Message | Send a direct message to all guild owners. | 81 | | Guilds | (Sort) | Get a complete list of guilds and info. | 82 | | Kill | | Kill the bot. It will remember this decision. | 83 | | Leave | (Guild) | Leave this guild and delete all associated information. | 84 | | Transfer | (Embed), Target Guild, (New Name) | Send an embed to another guild. | 85 | 86 | ## Utility 87 | | Commands | Arguments | Description | 88 | | -------------------- | --------- | ---------------------------------------- | 89 | | Help | (Command) | Display a help menu. | 90 | | Status, Ping, Uptime | | Display network status and total uptime. | 91 | 92 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gradle:6.7.0-jdk15 AS build 2 | COPY --chown=gradle:gradle . /embedbot 3 | WORKDIR /embedbot 4 | RUN gradle shadowJar --no-daemon 5 | 6 | FROM openjdk:8-jre-slim 7 | ENV BOT_TOKEN=UNSET 8 | RUN mkdir /config/ 9 | COPY --from=build /embedbot/build/libs/*.jar /EmbedBot.jar 10 | 11 | ENTRYPOINT ["java", "-jar", "/EmbedBot.jar", "$BOT_TOKEN"] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeJMattson/EmbedBot/5f49f5720f7be8d195ebce2cba93a8e9918e1d87/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /guide.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | 3 | Guide style: 4 | * The assumed prefix will be `e!`. 5 | * Commands use a readable format, but case does not matter (`Help` vs `help`). 6 | 7 | ## The Basics 8 | 9 | Before we start working, we need to create a new embed. You can do this with the `Create` command. 10 | * `e!Create hello` 11 | 12 | This creates a new embed, but it's empty. Let's add some text. 13 | * `e!SetTitle Hello World` 14 | 15 | Now you can display it on discord by sending it. 16 | * `e!Send` 17 | 18 | You've just created your first embed! 19 | 20 | ## Commands 21 | 22 | To find information about commands, you can use the `Help` feature. 23 | * `e!Help` will list all available commands. 24 | * `e!Help [Command]` will give help for a specific command. 25 | 26 | ## Editing Embeds 27 | 28 | If your embed is more permanent, like something in a #rules channel, we want to have a way to update it. 29 | 30 | The easiest way is to keep track of your embed when you send it. 31 | 32 | `e!Send true` 33 | 34 | This saves the message information. Once you make all of your changes, you can update it with one command. 35 | 36 | `e!Update` 37 | 38 | If you didn't track your embed when you first sent it, you can also update a message's embed. 39 | 40 | `e!UpdateTarget` 41 | 42 | 43 | ## Updating Sent Embeds 44 | 45 | ## Creating Multiple Embeds 46 | 47 | This bot can store as many embeds as you want, but this means that the bot needs to know which embed you're talking about when you run a command. It does this by keeping a single embed loaded. This allows you to edit embeds without having to specify the name each time. 48 | 49 | * You can run `e!ListEmbeds` to see all the embeds you have. 50 | * You can run `e!Load [Embed Name]` to load a different embed. 51 | 52 | ## Managing Multiple Embeds 53 | 54 | ## Importing Embeds 55 | 56 | ## Other useful commands -------------------------------------------------------------------------------- /setup.md: -------------------------------------------------------------------------------- 1 | ## Discord Setup 2 | 3 | Not interested in all the technical stuff? Just [invite](https://discordapp.com/oauth2/authorize?client_id=439163847618592782&scope=bot&permissions=101440) the bot directly. 4 | 5 | ### Bot Account 6 | Create a bot account in the [developers](https://discordapp.com/developers/applications/me) section of the Discord website. 7 | - Create an application 8 | - Under "General Information" 9 | - Enter an app icon and a name. 10 | - You will need the client ID when adding the bot to a server. 11 | - Under "Bot" 12 | - Create a bot. 13 | - Give it a username and app icon. 14 | - You will need the token when operating the bot. 15 | - This is a secret token, don't reveal it! 16 | - Save changes 17 | 18 | ### Add Bot 19 | - Visit the [permissions](https://discordapi.com/permissions.html) page. 20 | - Under "OAth URL Generator" enter the bot's client ID from the step above. 21 | - Click the link to add it to a server. 22 | 23 | ## Build Guide 24 | 25 | Choose one of the following build options. 26 | 27 |
28 | Source Code 29 | 30 | ### Prerequisites 31 | - [Download](https://github.com/JakeJMattson/EmbedBot/archive/master.zip) the code. 32 | - Install [Java](https://openjdk.java.net/) and [Gradle](https://gradle.org/install/). 33 | 34 | ### Building 35 | Once you have your prerequisites installed, Gradle will be doing the rest. 36 | 37 | * Navigate to the root of the project (`./EmbedBot`) 38 | * Run `gradle shadowJar` to build a JAR in `./build/libs/` 39 | * Proceed to the JAR guide below on how to run it. 40 | 41 |
42 | 43 |
44 | JAR/Release 45 | 46 | ### Prerequisites 47 | - Install [Java](https://openjdk.java.net/). 48 | - Download a JAR from [releases](https://github.com/JakeJMattson/EmbedBot/releases/), unless you built it yourself. 49 | 50 | ### Environment 51 | - To run the JAR, you will need to be able to access Java from the command line/terminal. Run `java -version` and make sure your operating system can recognize the command. 52 | - Place the JAR somewhere safe and out of the way. It will generate configuration files on its own. 53 | 54 | ### Running 55 | - Open the command prompt and navigate to the folder that the JAR is in. 56 | - Run the following command: `java -jar EmbedBot.jar ` 57 | - `` should be replaced with your Discord bot token 58 | 59 | - The bot should respond that configuration files have been generated. This will be in the `config` folder within the folder you created for this project. 60 | - Open `config.json` with any text editor and fill out the fields. You can read more about this below. 61 | - Run the same command again: `java -jar EmbedBot.jar token` 62 | 63 | The JAR will now read in your provided configuration values and start the bot. Your bot account should now be online! 64 | 65 |
66 |
67 | Docker 68 | 69 | New containers for this project are built automatically. Pulling it with Docker will always give you the most recent commit. 70 | 71 | * Install [Docker Desktop](https://www.docker.com/get-started) onto your machine. 72 | * Run `docker pull jakejmattson/embedbot` 73 | * Run `docker run -e BOT_TOKEN= -v :/config embedbot:latest` 74 | * `` should be replaced with the bot token obtained from the steps above. 75 | * `` should be replaced with a local path where you want to store the configuration files. 76 | 77 | Your Docker container should now be running. 78 | 79 |
80 | 81 | ## Configuration 82 | 83 | ```json 84 | { 85 | "botOwner": "The discord user with owner permissions.", 86 | "guildConfigurations": { 87 | "244230771232079873": { 88 | "prefix": "The prefix typed before a command.", 89 | "requiredRoleId": "The ID of the role required to use the bot." 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | ### Sanity Check 96 | Once you have your bot account online, you can verify that it's working by mentioning the bot on Discord `@EmbedBot`. Make sure it has permissions to read and write in the place where you want it to respond. It is recommended that you give it a role. -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/MainApp.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot 2 | 3 | import me.jakejmattson.discordkt.api.dsl.bot 4 | import me.jakejmattson.discordkt.api.extensions.jda.* 5 | import me.jakejmattson.embedbot.dataclasses.Configuration 6 | import me.jakejmattson.embedbot.extensions.requiredPermissionLevel 7 | import me.jakejmattson.embedbot.services.PermissionsService 8 | import java.awt.Color 9 | 10 | fun main(args: Array) { 11 | val token = args.firstOrNull() 12 | ?: throw IllegalArgumentException("No program arguments provided. Expected bot token.") 13 | 14 | bot(token) { 15 | configure { 16 | val (configuration, permissionsService) 17 | = it.getInjectionObjects(Configuration::class, PermissionsService::class) 18 | 19 | commandReaction = null 20 | allowMentionPrefix = true 21 | 22 | prefix { 23 | it.guild?.let { configuration[it.idLong]?.prefix } ?: "" 24 | } 25 | 26 | colors { 27 | infoColor = Color(0x00BFFF) 28 | } 29 | 30 | mentionEmbed { 31 | val guild = it.guild ?: return@mentionEmbed 32 | val jda = it.discord.jda 33 | val properties = it.discord.properties 34 | val prefix = it.relevantPrefix 35 | val role = configuration[guild.idLong]?.getLiveRole(jda)?.takeUnless { it == guild.publicRole }?.asMention 36 | ?: "" 37 | 38 | author { 39 | jda.retrieveUserById(254786431656919051).queue { user -> 40 | iconUrl = user.effectiveAvatarUrl 41 | name = user.fullName() 42 | url = user.profileLink 43 | } 44 | } 45 | 46 | simpleTitle = "EmbedBot" 47 | thumbnail = jda.selfUser.effectiveAvatarUrl 48 | color = infoColor 49 | description = (if (role.isNotBlank()) "You must have $role" else "") + 50 | "\nCurrent prefix is `$prefix`" + 51 | "\nUse `${prefix}help` to see commands." 52 | 53 | addInlineField("", "[[Invite Me]](https://discordapp.com/oauth2/authorize?client_id=439163847618592782&scope=bot&permissions=101440)") 54 | addInlineField("", "[[See Code]](https://github.com/JakeJMattson/EmbedBot)") 55 | addInlineField("", "[[User Guide]](https://github.com/JakeJMattson/EmbedBot/blob/master/guide.md)") 56 | 57 | footer { 58 | text = "2.1.0 - ${properties.libraryVersion} - ${properties.jdaVersion}" 59 | } 60 | } 61 | 62 | visibilityPredicate { 63 | val guild = it.guild ?: return@visibilityPredicate false 64 | val member = it.user.toMember(guild)!! 65 | val permission = it.command.requiredPermissionLevel 66 | 67 | permissionsService.hasClearance(member, permission) 68 | } 69 | 70 | it.jda.guilds.forEach { 71 | configuration.setup(it) 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/arguments/EmbedArg.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.arguments 2 | 3 | import me.jakejmattson.discordkt.api.dsl.arguments.* 4 | import me.jakejmattson.discordkt.api.dsl.command.CommandEvent 5 | import me.jakejmattson.embedbot.dataclasses.Embed 6 | import me.jakejmattson.embedbot.extensions.* 7 | import me.jakejmattson.embedbot.utils.messages 8 | 9 | open class EmbedArg(override val name: String = "Embed") : ArgumentType() { 10 | companion object : EmbedArg() 11 | 12 | override fun convert(arg: String, args: List, event: CommandEvent<*>): ArgumentResult { 13 | val guild = event.guild ?: return Error(messages.MISSING_GUILD) 14 | 15 | val embed = guild.getEmbedByName(arg) 16 | ?: return Error("No such embed exists with the name: $arg") 17 | 18 | return Success(embed) 19 | } 20 | 21 | override fun generateExamples(event: CommandEvent<*>) = event.guild?.getEmbeds()?.map { it.name } 22 | ?: listOf("") 23 | 24 | override fun formatData(data: Embed) = data.name 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/arguments/FieldArg.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.arguments 2 | 3 | import me.jakejmattson.discordkt.api.dsl.arguments.* 4 | import me.jakejmattson.discordkt.api.dsl.command.CommandEvent 5 | import me.jakejmattson.embedbot.extensions.getLoadedEmbed 6 | import me.jakejmattson.embedbot.services.Field 7 | import me.jakejmattson.embedbot.utils.* 8 | 9 | open class FieldArg(override val name: String = "Field Data", private val delimiter: String = "|") : ArgumentType() { 10 | companion object : FieldArg() 11 | 12 | override fun convert(arg: String, args: List, event: CommandEvent<*>): ArgumentResult { 13 | val guild = event.guild ?: return Error(messages.MISSING_GUILD) 14 | val data = args.joinToString(" ").split(delimiter) 15 | 16 | guild.getLoadedEmbed() ?: return Error(messages.MISSING_EMBED) 17 | 18 | if (data.size !in 2..3) 19 | return Error("Invalid field data. Expected 2-3 items split by \"$delimiter\". Received ${data.size}") 20 | 21 | val name = data.component1() 22 | val value = data.component2() 23 | val inline = if (data.size == 3) data.component3().toBoolean() else false 24 | 25 | if (name.length > FIELD_NAME_LIMIT) 26 | return Error("Max field name length is $FIELD_NAME_LIMIT characters. Input was ${name.length}.") 27 | 28 | if (value.length > FIELD_VALUE_LIMIT) 29 | return Error("Max field value length is $FIELD_VALUE_LIMIT characters. Input was ${value.length}.") 30 | 31 | val field = Field(name, value, inline) 32 | 33 | return Success(field, args.size) 34 | } 35 | 36 | override fun generateExamples(event: CommandEvent<*>) = listOf("Title|Body") 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/arguments/FieldIndexArg.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.arguments 2 | 3 | import me.jakejmattson.discordkt.api.dsl.arguments.* 4 | import me.jakejmattson.discordkt.api.dsl.command.CommandEvent 5 | import me.jakejmattson.embedbot.extensions.getLoadedEmbed 6 | import me.jakejmattson.embedbot.utils.messages 7 | 8 | open class FieldIndexArg(override val name: String = "Field Index") : ArgumentType() { 9 | companion object : FieldIndexArg() 10 | 11 | override fun convert(arg: String, args: List, event: CommandEvent<*>): ArgumentResult { 12 | val guild = event.guild ?: return Error(messages.MISSING_GUILD) 13 | 14 | val embed = guild.getLoadedEmbed() 15 | ?: return Error(messages.MISSING_EMBED) 16 | 17 | if (embed.fieldCount == 0) 18 | return Error("This embed has no fields.") 19 | 20 | val index = arg.toIntOrNull() 21 | ?: return Error("Expected an integer, got $arg") 22 | 23 | if (index !in 0 until embed.fieldCount) 24 | return Error("Invalid index. Expected range: 0-${embed.fieldCount - 1}") 25 | 26 | return Success(index) 27 | } 28 | 29 | override fun generateExamples(event: CommandEvent<*>): List { 30 | val maxIndex = event.guild?.getLoadedEmbed()?.fieldCount ?: 0 31 | return listOf((0..maxIndex).random().toString()) 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/arguments/GroupArg.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.arguments 2 | 3 | import me.jakejmattson.discordkt.api.dsl.arguments.* 4 | import me.jakejmattson.discordkt.api.dsl.command.CommandEvent 5 | import me.jakejmattson.embedbot.dataclasses.Group 6 | import me.jakejmattson.embedbot.extensions.* 7 | import me.jakejmattson.embedbot.utils.messages 8 | 9 | open class GroupArg(override val name: String = "Group") : ArgumentType() { 10 | companion object : GroupArg() 11 | 12 | override fun convert(arg: String, args: List, event: CommandEvent<*>): ArgumentResult { 13 | val guild = event.guild ?: return Error(messages.MISSING_GUILD) 14 | 15 | val guildGroup = guild.getGroupByName(arg) 16 | ?: return Error("No such group exists with the name: $arg") 17 | 18 | return Success(guildGroup) 19 | } 20 | 21 | override fun generateExamples(event: CommandEvent<*>) = event.guild?.getGroups()?.map { it.name } 22 | ?: listOf("") 23 | 24 | override fun formatData(data: Group) = data.name 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/commands/CoreCommands.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.commands 2 | 3 | import com.google.gson.JsonSyntaxException 4 | import me.jakejmattson.discordkt.api.annotations.CommandSet 5 | import me.jakejmattson.discordkt.api.arguments.* 6 | import me.jakejmattson.discordkt.api.dsl.command.* 7 | import me.jakejmattson.discordkt.api.dsl.embed.toEmbedBuilder 8 | import me.jakejmattson.discordkt.internal.command.ParseResult 9 | import me.jakejmattson.embedbot.arguments.EmbedArg 10 | import me.jakejmattson.embedbot.dataclasses.* 11 | import me.jakejmattson.embedbot.extensions.* 12 | import me.jakejmattson.embedbot.services.* 13 | import me.jakejmattson.embedbot.utils.messages 14 | import net.dv8tion.jda.api.entities.TextChannel 15 | 16 | @CommandSet("Core") 17 | fun coreCommands(embedService: EmbedService, permissionsService: PermissionsService) = commands { 18 | command("Send") { 19 | description = "Send the currently loaded embed." 20 | requiresLoadedEmbed = true 21 | execute(TextChannelArg("Channel").makeOptional { it.channel as TextChannel }, 22 | BooleanArg("saveLocation").makeOptional(false)) { 23 | val (channel, saveLocation) = it.args 24 | val embed = it.guild!!.getLoadedEmbed()!! 25 | 26 | if (embed.isEmpty) 27 | return@execute it.respond(messages.EMPTY_EMBED) 28 | 29 | channel.sendMessage(embed.build()).queue { message -> 30 | if (saveLocation) 31 | embed.location = Location(channel.id, message.id) 32 | } 33 | 34 | if (channel != it.channel) 35 | it.reactSuccess() 36 | } 37 | } 38 | 39 | command("Create") { 40 | description = "Create a new embed with this name." 41 | execute(AnyArg("Embed Name")) { 42 | val embedName = it.args.first 43 | val wasCreated = embedService.createEmbed(it.guild!!, embedName) 44 | 45 | if (!wasCreated) 46 | return@execute it.respond(messages.EMBED_ALREADY_EXISTS) 47 | 48 | it.reactSuccess() 49 | } 50 | } 51 | 52 | command("Duplicate") { 53 | description = "Create a new embed from an existing embed." 54 | execute(AnyArg("Embed Name"), EmbedArg.makeNullableOptional { it.guild!!.getLoadedEmbed() }) { 55 | val embedName = it.args.first 56 | val existingEmbed = it.args.second ?: return@execute it.respond(messages.MISSING_OPTIONAL_EMBED) 57 | val embed = createEmbedFromJson(embedName, existingEmbed.toJson()) 58 | val wasCreated = embedService.addEmbed(it.guild!!, embed) 59 | 60 | if (!wasCreated) 61 | it.respond(messages.EMBED_ALREADY_EXISTS) 62 | 63 | it.reactSuccess() 64 | } 65 | } 66 | 67 | command("Delete") { 68 | description = "Delete the embed with this name." 69 | execute(MultipleArg(EmbedArg).makeNullableOptional { it.guild!!.getLoadedEmbed()?.let { listOf(it) } }) { event -> 70 | val embeds = event.args.first ?: return@execute event.respond(messages.MISSING_OPTIONAL_EMBED) 71 | 72 | embeds.forEach { event.guild!!.removeEmbed(it) } 73 | event.reactSuccess() 74 | } 75 | } 76 | 77 | command("Load") { 78 | description = "Load the embed with this name into memory." 79 | execute(EmbedArg) { 80 | val embed = it.args.first 81 | it.guild!!.loadEmbed(embed) 82 | it.reactSuccess() 83 | } 84 | } 85 | 86 | command("Import") { 87 | description = "Import a JSON file or string as an embed." 88 | execute(AnyArg("Embed Name"), FileArg("File") or EveryArg("String")) { 89 | val (name, input) = it.args 90 | val json = input.map({ file -> file.readText().also { file.delete() } }, { it }) 91 | 92 | it.importJson(name, json, embedService) 93 | } 94 | } 95 | 96 | command("Export") { 97 | description = "Export the currently loaded embed to JSON." 98 | execute(EmbedArg.makeNullableOptional { it.guild!!.getLoadedEmbed() }, BooleanArg("preferFile").makeOptional(false)) { 99 | val embed = it.args.first 100 | ?: return@execute it.respond(messages.MISSING_OPTIONAL_EMBED) 101 | 102 | val preferFile = it.args.second 103 | val json = embed.toJson() 104 | 105 | if (preferFile || json.length >= 1985) 106 | it.channel.sendFile(json.toByteArray(), "$${embed.name}.json").queue() 107 | else 108 | it.respond("```json\n$json```") 109 | } 110 | } 111 | 112 | command("Copy") { 113 | description = "Copy an embed by its message ID." 114 | execute(AnyArg("Embed Name"), MessageArg("Message Link or ID")) { 115 | val (name, message) = it.args 116 | 117 | val guild = it.guild!!.takeIf { !it.hasEmbedWithName(name) } 118 | ?: return@execute it.respond(messages.EMBED_ALREADY_EXISTS) 119 | 120 | val builder = message.getEmbed()?.toEmbedBuilder() 121 | ?: return@execute it.respond("Target message has no embed.") 122 | 123 | val embed = Embed(name, builder, Location(message.channel.id, message.id)) 124 | 125 | embedService.addEmbed(guild, embed) 126 | it.reactSuccess() 127 | } 128 | } 129 | 130 | command("Update") { 131 | description = "Update the message embed" 132 | requiresLoadedEmbed = true 133 | execute { 134 | val embed = it.guild!!.getLoadedEmbed()!! 135 | val original = embed.location ?: return@execute it.respond(messages.NO_LOCATION) 136 | val updateResponse = embed.update(it.discord, original.channelId, original.messageId) 137 | 138 | if (!updateResponse.wasSuccessful) 139 | return@execute it.respond(updateResponse.message) 140 | 141 | it.reactSuccess() 142 | } 143 | } 144 | 145 | command("UpdateTarget") { 146 | description = "Replace the target message embed with the loaded embed." 147 | requiresLoadedEmbed = true 148 | execute(MessageArg("Message Link or ID")) { 149 | val message = it.args.first 150 | val embed = it.guild!!.getLoadedEmbed()!! 151 | val updateResponse = embed.update(it.discord, message.channel.id, message.id) 152 | 153 | if (!updateResponse.wasSuccessful) 154 | return@execute it.respond(updateResponse.message) 155 | 156 | it.reactSuccess() 157 | } 158 | } 159 | 160 | command("ExecuteAll") { 161 | description = "Execute a batch of commands in sequence." 162 | execute(EveryArg("Commands")) { event -> 163 | val rawInvocations = event.args.first.split("\n").filter { it.isNotEmpty() } 164 | val invalidCommands = mutableListOf() 165 | 166 | val commandMap = rawInvocations.mapNotNull { 167 | val split = it.split(" ") 168 | val commandName = split.first() 169 | val args = split.drop(1) 170 | 171 | event.container[commandName]?.let { 172 | val hasPermission = permissionsService.hasClearance(event.message.member!!, it.requiredPermissionLevel) 173 | 174 | if (hasPermission) 175 | it to args 176 | else 177 | null 178 | } 179 | } 180 | 181 | if (invalidCommands.isNotEmpty()) 182 | return@execute event.respond("Invalid: ${invalidCommands.joinToString()}") 183 | 184 | if (commandMap.isEmpty()) 185 | return@execute event.respond("No Commands!") 186 | 187 | val genericEvent = event.cloneToGeneric() 188 | 189 | val (success, fail) = commandMap.map { (command, args) -> 190 | command to command.manualParseInput(args, genericEvent) 191 | }.partition { it.second is ParseResult.Success } 192 | 193 | if (fail.isNotEmpty()) { 194 | val response = fail.joinToString { it.first.names.first() } 195 | return@execute event.respond("Failed: $response") 196 | } 197 | 198 | println(success.joinToString { it.first.names.first() }) 199 | 200 | success 201 | .map { it.first to (it.second as ParseResult.Success).argumentContainer } 202 | .forEach { (command, result) -> 203 | println("${command.names.first()}(${result})") 204 | command.manualInvoke(result, genericEvent) 205 | } 206 | 207 | event.reactSuccess() 208 | } 209 | } 210 | } 211 | 212 | private fun CommandEvent<*>.importJson(name: String, json: String, embedService: EmbedService) { 213 | val guild = guild!! 214 | 215 | if (guild.hasEmbedWithName(name)) 216 | return respond(messages.EMBED_ALREADY_EXISTS) 217 | 218 | try { 219 | val embed = createEmbedFromJson(name, json) 220 | val wasAdded = embedService.addEmbed(guild, embed) 221 | 222 | if (!wasAdded) 223 | respond(messages.EMBED_ALREADY_EXISTS) 224 | 225 | reactSuccess() 226 | } catch (e: JsonSyntaxException) { 227 | respond("Invalid JSON! ${e.message?.substringAfter("Exception: ")}") 228 | } 229 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/commands/EditCommands.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.commands 2 | 3 | import me.jakejmattson.discordkt.api.annotations.CommandSet 4 | import me.jakejmattson.discordkt.api.arguments.* 5 | import me.jakejmattson.discordkt.api.dsl.command.commands 6 | import me.jakejmattson.embedbot.arguments.EmbedArg 7 | import me.jakejmattson.embedbot.extensions.* 8 | import me.jakejmattson.embedbot.utils.* 9 | import java.time.LocalDateTime 10 | 11 | @CommandSet("Edit") 12 | fun editCommands() = commands { 13 | command("SetAuthor") { 14 | description = "Set the author for the currently loaded embed." 15 | requiresLoadedEmbed = true 16 | execute(UserArg) { 17 | val user = it.args.first 18 | val embed = it.guild!!.getLoadedEmbed()!! 19 | 20 | embed.setAuthor(user.name, user.effectiveAvatarUrl) 21 | it.reactSuccess() 22 | } 23 | } 24 | 25 | command("SetColor") { 26 | description = "Set the color for the currently loaded embed." 27 | requiresLoadedEmbed = true 28 | execute(HexColorArg) { 29 | val color = it.args.first 30 | val embed = it.guild!!.getLoadedEmbed()!! 31 | 32 | embed.setColor(color) 33 | it.reactSuccess() 34 | } 35 | } 36 | 37 | command("SetDescription") { 38 | description = "Set the description for the currently loaded embed." 39 | requiresLoadedEmbed = true 40 | execute(EveryArg) { 41 | val description = it.args.first 42 | val embed = it.guild!!.getLoadedEmbed()!! 43 | 44 | if (description.length > DESCRIPTION_LIMIT) 45 | return@execute it.respond("Max description length is $DESCRIPTION_LIMIT characters. Input was ${description.length}.") 46 | 47 | embed.setDescription(description) 48 | it.reactSuccess() 49 | } 50 | } 51 | 52 | command("SetFooter") { 53 | description = "Set the footer for the currently loaded embed." 54 | requiresLoadedEmbed = true 55 | execute(UrlArg("Icon URL"), EveryArg("Text")) { 56 | val (url, text) = it.args 57 | val embed = it.guild!!.getLoadedEmbed()!! 58 | 59 | if (text.length > FOOTER_LIMIT) 60 | return@execute it.respond("Max footer length is $FOOTER_LIMIT characters. Input was ${text.length}.") 61 | 62 | embed.setFooter(text, url) 63 | it.reactSuccess() 64 | } 65 | } 66 | 67 | command("SetImage") { 68 | description = "Set the image for the currently loaded embed." 69 | requiresLoadedEmbed = true 70 | execute(UrlArg) { 71 | val (url) = it.args 72 | val embed = it.guild!!.getLoadedEmbed()!! 73 | 74 | embed.setImage(url) 75 | it.reactSuccess() 76 | } 77 | } 78 | 79 | command("SetThumbnail") { 80 | description = "Set the thumbnail for the currently loaded embed." 81 | requiresLoadedEmbed = true 82 | execute(UrlArg) { 83 | val url = it.args.first 84 | val embed = it.guild!!.getLoadedEmbed()!! 85 | 86 | embed.setThumbnail(url) 87 | it.reactSuccess() 88 | } 89 | } 90 | 91 | command("SetTimestamp") { 92 | description = "Set the timestamp for the currently loaded embed." 93 | requiresLoadedEmbed = true 94 | execute { 95 | val embed = it.guild!!.getLoadedEmbed()!! 96 | 97 | embed.setTimestamp(LocalDateTime.now()) 98 | it.reactSuccess() 99 | } 100 | } 101 | 102 | command("SetTitle") { 103 | description = "Set the title for the currently loaded embed." 104 | requiresLoadedEmbed = true 105 | execute(EveryArg) { 106 | val title = it.args.first 107 | val embed = it.guild!!.getLoadedEmbed()!! 108 | 109 | if (title.length > TITLE_LIMIT) 110 | return@execute it.respond("Max title limit is $TITLE_LIMIT characters. Input was ${title.length}.") 111 | 112 | embed.setTitle(title) 113 | it.reactSuccess() 114 | } 115 | } 116 | 117 | command("Clear") { 118 | description = "Clear a target field from the loaded embed." 119 | requiresLoadedEmbed = true 120 | execute(AnyArg("Clear Target").makeOptional("")) { 121 | val field = it.args.first.toLowerCase() 122 | val embed = it.guild!!.getLoadedEmbed()!! 123 | val options = "Options:\nAuthor, Color, Description, Footer, Image, Thumbnail, Timestamp, Title \nAll, Fields, Non-Fields" 124 | 125 | with(embed) { 126 | when (field) { 127 | "author" -> clearAuthor() 128 | "color" -> clearColor() 129 | "description" -> clearDescription() 130 | "footer" -> clearFooter() 131 | "image" -> clearImage() 132 | "thumbnail" -> clearThumbnail() 133 | "timestamp" -> clearTimestamp() 134 | "title" -> clearTitle() 135 | 136 | "all" -> clear() 137 | "fields" -> clearFields() 138 | "non-fields" -> clearNonFields() 139 | else -> null 140 | } 141 | } ?: return@execute it.respond("Invalid field selected. $options") 142 | 143 | it.reactSuccess() 144 | } 145 | } 146 | 147 | command("Rename") { 148 | description = "Change the name of an existing embed." 149 | execute(EmbedArg.makeNullableOptional { it.guild!!.getLoadedEmbed() }, AnyArg("New Name")) { 150 | val targetEmbed = it.args.first 151 | ?: return@execute it.respond(messages.MISSING_OPTIONAL_EMBED) 152 | 153 | val newName = it.args.second 154 | val guild = it.guild!! 155 | 156 | if (guild.hasEmbedWithName(newName)) 157 | return@execute it.respond(messages.EMBED_ALREADY_EXISTS) 158 | 159 | if (targetEmbed.isLoaded(guild)) { 160 | val updatedEmbed = guild.getEmbedByName(targetEmbed.name)!! 161 | updatedEmbed.name = newName 162 | guild.loadEmbed(updatedEmbed) 163 | } else { 164 | targetEmbed.name = newName 165 | } 166 | 167 | it.reactSuccess() 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/commands/FieldCommands.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.commands 2 | 3 | import me.jakejmattson.discordkt.api.annotations.CommandSet 4 | import me.jakejmattson.discordkt.api.arguments.* 5 | import me.jakejmattson.discordkt.api.dsl.command.commands 6 | import me.jakejmattson.embedbot.arguments.* 7 | import me.jakejmattson.embedbot.extensions.* 8 | import me.jakejmattson.embedbot.utils.* 9 | 10 | @CommandSet("Field") 11 | fun fieldCommands() = commands { 12 | command("AddField") { 13 | description = "Add a field in the following format: title|body|inline" 14 | requiresLoadedEmbed = true 15 | execute(FieldArg) { 16 | val field = it.args.first 17 | val embed = it.guild!!.getLoadedEmbed()!! 18 | 19 | if (embed.fieldCount == FIELD_LIMIT) 20 | return@execute it.respond("Embeds can only hold $FIELD_LIMIT fields.") 21 | 22 | embed.addField(field) 23 | it.reactSuccess() 24 | } 25 | } 26 | 27 | command("AddBlankField") { 28 | description = "Add a blank field to the loaded embed." 29 | requiresLoadedEmbed = true 30 | execute(BooleanArg("isInline").makeOptional(false)) { 31 | val isInline = it.args.first 32 | val embed = it.guild!!.getLoadedEmbed()!! 33 | 34 | if (embed.fieldCount == FIELD_LIMIT) 35 | return@execute it.respond("Embeds can only hold $FIELD_LIMIT fields.") 36 | 37 | embed.addBlankField(isInline) 38 | it.reactSuccess() 39 | } 40 | } 41 | 42 | command("InsertField") { 43 | description = "Insert a field at an index to the loaded embed." 44 | requiresLoadedEmbed = true 45 | execute(FieldIndexArg("Index"), FieldArg) { 46 | val (index, field) = it.args 47 | val embed = it.guild!!.getLoadedEmbed()!! 48 | 49 | if (embed.fieldCount == FIELD_LIMIT) 50 | return@execute it.respond("Embeds can only hold $FIELD_LIMIT fields.") 51 | 52 | embed.insertField(index, field) 53 | it.reactSuccess() 54 | } 55 | } 56 | 57 | command("RemoveField") { 58 | description = "Remove a field from the loaded embed by its index." 59 | requiresLoadedEmbed = true 60 | execute(FieldIndexArg) { 61 | val index = it.args.first 62 | val embed = it.guild!!.getLoadedEmbed()!! 63 | 64 | embed.removeField(index) 65 | it.reactSuccess() 66 | } 67 | } 68 | 69 | command("SetField") { 70 | description = "Edit a field at a given index with the given data." 71 | requiresLoadedEmbed = true 72 | execute(FieldIndexArg, FieldArg) { 73 | val (index, field) = it.args 74 | val embed = it.guild!!.getLoadedEmbed()!! 75 | 76 | embed.setField(index, field) 77 | it.reactSuccess() 78 | } 79 | } 80 | 81 | command("SetFieldTitle") { 82 | description = "Get a field by its index and edit its title value." 83 | requiresLoadedEmbed = true 84 | execute(FieldIndexArg, EveryArg) { 85 | val (index, newTitle) = it.args 86 | val embed = it.guild!!.getLoadedEmbed()!! 87 | 88 | if (newTitle.length > FIELD_NAME_LIMIT) 89 | return@execute it.respond("Max field name length is $FIELD_NAME_LIMIT characters. Input was ${newTitle.length}.") 90 | 91 | embed.setFieldName(index, newTitle) 92 | it.reactSuccess() 93 | } 94 | } 95 | 96 | command("SetFieldText") { 97 | description = "Get a field by its index and edit its text value." 98 | requiresLoadedEmbed = true 99 | execute(FieldIndexArg, EveryArg) { 100 | val (index, newText) = it.args 101 | val embed = it.guild!!.getLoadedEmbed()!! 102 | 103 | if (newText.length > FIELD_VALUE_LIMIT) 104 | return@execute it.respond("Max field value length is $FIELD_VALUE_LIMIT characters. Input was ${newText.length}.") 105 | 106 | embed.setFieldText(index, newText) 107 | it.reactSuccess() 108 | } 109 | } 110 | 111 | command("SetFieldInline") { 112 | description = "Get a field by its index and edit its inline value." 113 | requiresLoadedEmbed = true 114 | execute(FieldIndexArg, BooleanArg) { 115 | val (index, newInline) = it.args 116 | val embed = it.guild!!.getLoadedEmbed()!! 117 | 118 | embed.setFieldInline(index, newInline) 119 | it.reactSuccess() 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/commands/GroupCommands.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.commands 2 | 3 | import me.jakejmattson.discordkt.api.annotations.CommandSet 4 | import me.jakejmattson.discordkt.api.arguments.* 5 | import me.jakejmattson.discordkt.api.dsl.command.commands 6 | import me.jakejmattson.discordkt.api.dsl.embed.toEmbedBuilder 7 | import me.jakejmattson.embedbot.arguments.* 8 | import me.jakejmattson.embedbot.dataclasses.* 9 | import me.jakejmattson.embedbot.extensions.* 10 | import me.jakejmattson.embedbot.services.EmbedService 11 | import me.jakejmattson.embedbot.utils.messages 12 | import net.dv8tion.jda.api.entities.TextChannel 13 | 14 | @CommandSet("Group") 15 | fun groupCommands(embedService: EmbedService) = commands { 16 | command("CreateGroup") { 17 | description = "Create a group of embeds." 18 | execute(AnyArg("Group Name"), MultipleArg(EmbedArg).makeOptional(listOf())) { 19 | val (groupName, embeds) = it.args 20 | val guild = it.guild!! 21 | val group = embedService.createGroup(guild, groupName) 22 | ?: return@execute it.respond(messages.GROUP_ALREADY_EXISTS) 23 | 24 | embeds.forEach { embed -> 25 | group.addEmbed(guild, embed) 26 | } 27 | 28 | it.reactSuccess() 29 | } 30 | } 31 | 32 | command("DeleteGroup") { 33 | description = "Delete a group and all of its embeds." 34 | execute(GroupArg) { 35 | val group = it.args.first 36 | val wasDeleted = embedService.deleteGroup(it.guild!!, group) 37 | 38 | if (!wasDeleted) 39 | it.respond("No such group with this name.") 40 | 41 | it.reactSuccess() 42 | } 43 | } 44 | 45 | command("CloneGroup") { 46 | description = "Clone a group of embeds." 47 | execute(AnyArg("Group Name"), 48 | TextChannelArg("Channel").makeOptional { it.channel as TextChannel }, 49 | IntegerArg("Amount")) { event -> 50 | val (groupName, channel, amount) = event.args 51 | 52 | if (amount <= 0) 53 | return@execute event.respond("Group size should be 1 or greater.") 54 | 55 | val embeds = 56 | channel.iterableHistory.complete() 57 | .filter { it.getEmbed() != null } 58 | .take(amount) 59 | .reversed() 60 | .mapIndexed { index, message -> 61 | Embed("$groupName-${index + 1}", message.getEmbed()!!.toEmbedBuilder(), Location(channel.id, message.id)) 62 | } as MutableList 63 | 64 | val wasSuccessful = embedService.createGroupFromEmbeds(event.guild!!, Group(groupName, embeds)) 65 | 66 | if (!wasSuccessful) 67 | event.respond(messages.GROUP_ALREADY_EXISTS) 68 | 69 | event.respond("Cloned ${embeds.size} embeds into $groupName") 70 | } 71 | } 72 | 73 | command("UpdateGroup") { 74 | description = "Update the original embeds this group was copied from." 75 | execute(GroupArg) { event -> 76 | val group = event.args.first 77 | val failures = mutableListOf() 78 | val size = group.size 79 | 80 | val totalSuccessful = group.embeds.sumBy { embed -> 81 | val location = embed.location 82 | 83 | if (location == null) { 84 | failures.add(messages.NO_LOCATION) 85 | return@sumBy 0 86 | } 87 | 88 | val updateResponse = embed.update(event.discord, location.channelId, location.messageId) 89 | 90 | with(updateResponse) { 91 | if (!wasSuccessful) 92 | failures.add("${embed.name} :: ${updateResponse.message}") 93 | 94 | if (wasSuccessful) 1 else 0 95 | } 96 | } 97 | 98 | if (totalSuccessful == size) 99 | return@execute event.respond("Successfully updated all $size embeds in ${group.name}") 100 | 101 | event.respond("Successfully updated $totalSuccessful out of $size in ${group.name}" + 102 | "\nFailed the following updates:\n${failures.joinToString("\n")}") 103 | } 104 | } 105 | 106 | command("RenameGroup") { 107 | description = "Change the name of an existing group." 108 | execute(GroupArg, AnyArg("New Name")) { 109 | val (group, newName) = it.args 110 | 111 | if (it.guild!!.hasGroupWithName(newName)) 112 | return@execute it.respond(messages.GROUP_ALREADY_EXISTS) 113 | 114 | group.name = newName 115 | it.reactSuccess() 116 | } 117 | } 118 | 119 | command("Deploy") { 120 | description = "Deploy a group into a target channel." 121 | execute(GroupArg, 122 | TextChannelArg("Channel").makeOptional { it.channel as TextChannel }, 123 | BooleanArg("saveLocation").makeOptional(false)) { 124 | val (group, channel, saveLocation) = it.args 125 | 126 | group.embeds.forEach { embed -> 127 | if (!embed.isEmpty) { 128 | channel.sendMessage(embed.build()).queue { message -> 129 | if (saveLocation) 130 | embed.location = Location(channel.id, message.id) 131 | } 132 | } 133 | } 134 | 135 | if (channel != it.channel) 136 | it.reactSuccess() 137 | } 138 | } 139 | 140 | command("AddToGroup") { 141 | description = "Add an embed into a group." 142 | execute(GroupArg, MultipleArg(EmbedArg)) { 143 | val (group, embeds) = it.args 144 | val guild = it.guild!! 145 | 146 | embeds.forEach { embed -> 147 | group.addEmbed(guild, embed) 148 | } 149 | 150 | it.reactSuccess() 151 | } 152 | } 153 | 154 | command("InsertIntoGroup") { 155 | description = "Insert an embed into a group at an index." 156 | execute(GroupArg, IntegerArg("Index"), EmbedArg) { 157 | val (group, index, embed) = it.args 158 | 159 | if (index !in 0..group.size) 160 | return@execute it.respond("Invalid Index. Expected range: 0-${group.size}") 161 | 162 | group.addEmbed(it.guild!!, embed, index) 163 | 164 | it.reactSuccess() 165 | } 166 | } 167 | 168 | command("RemoveFromGroup") { 169 | description = "Remove embeds from their current group." 170 | execute(MultipleArg(EmbedArg)) { 171 | val embeds = it.args.first 172 | 173 | embeds.forEach { embed -> 174 | embedService.removeEmbedFromGroup(it.guild!!, embed) 175 | } 176 | 177 | it.reactSuccess() 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/commands/GuildConfigurationCommands.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.commands 2 | 3 | import me.jakejmattson.discordkt.api.annotations.CommandSet 4 | import me.jakejmattson.discordkt.api.arguments.* 5 | import me.jakejmattson.discordkt.api.dsl.command.commands 6 | import me.jakejmattson.embedbot.dataclasses.Configuration 7 | import me.jakejmattson.embedbot.extensions.requiredPermissionLevel 8 | import me.jakejmattson.embedbot.services.* 9 | 10 | @CommandSet("GuildConfiguration") 11 | fun guildConfigurationCommands(configuration: Configuration, embedService: EmbedService) = commands { 12 | command("SetPrefix") { 13 | description = "Set the prefix required for the bot to register a command." 14 | requiredPermissionLevel = Permission.GUILD_OWNER 15 | execute(AnyArg("Prefix")) { 16 | val prefix = it.args.first 17 | 18 | configuration[it.guild!!.idLong]?.prefix = prefix 19 | configuration.save() 20 | 21 | it.respond("Prefix set to: $prefix") 22 | } 23 | } 24 | 25 | command("SetRole") { 26 | description = "Set the role required to use this bot." 27 | requiredPermissionLevel = Permission.GUILD_OWNER 28 | execute(RoleArg) { 29 | val requiredRole = it.args.first 30 | 31 | configuration[it.guild!!.idLong]?.requiredRoleId = requiredRole.idLong 32 | configuration.save() 33 | 34 | it.respond("Required role set to: ${requiredRole.name}") 35 | } 36 | } 37 | 38 | command("DeleteAll") { 39 | description = "Delete all embeds and groups in this guild." 40 | requiredPermissionLevel = Permission.GUILD_OWNER 41 | execute { 42 | val guild = it.guild!! 43 | val removed = embedService.removeAllFromGuild(guild) 44 | 45 | it.respond("Successfully deleted $removed embeds.") 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/commands/InfoCommands.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.commands 2 | 3 | import me.jakejmattson.discordkt.api.annotations.CommandSet 4 | import me.jakejmattson.discordkt.api.dsl.command.commands 5 | import me.jakejmattson.embedbot.arguments.EmbedArg 6 | import me.jakejmattson.embedbot.extensions.* 7 | import me.jakejmattson.embedbot.utils.* 8 | import java.awt.Color 9 | 10 | @CommandSet("Information") 11 | fun infoCommands() = commands { 12 | command("Info") { 13 | description = "Get extended info for the target embed." 14 | execute(EmbedArg.makeNullableOptional { it.guild!!.getLoadedEmbed() }) { 15 | val embed = it.args.first 16 | ?: return@execute it.respond(messages.MISSING_OPTIONAL_EMBED) 17 | 18 | val guild = it.guild!! 19 | 20 | it.respond { 21 | color = Color(0x00bfff) 22 | addField("Embed Name", embed.name) 23 | addField("Is Loaded", embed.isLoaded(guild).toString()) 24 | 25 | if (!embed.isEmpty) { 26 | addField("Field Count", "${embed.fieldCount}/$FIELD_LIMIT") 27 | addField("Character Count", "${embed.charCount}/$CHAR_LIMIT") 28 | } else { 29 | addField("Is Empty", embed.isEmpty.toString()) 30 | } 31 | 32 | addField("Copied From", embed.locationString) 33 | } 34 | } 35 | } 36 | 37 | command("ListEmbeds") { 38 | description = "List all embeds created in this guild." 39 | execute { event -> 40 | event.respond { 41 | val guild = event.guild!! 42 | val embeds = guild.getEmbeds() 43 | val groups = guild.getGroups() 44 | val loadedEmbed = guild.getLoadedEmbed() 45 | val embedList = embeds.joinToString("\n") { it.name }.takeIf { it.isNotEmpty() } ?: "" 46 | 47 | color = Color(0x00bfff) 48 | 49 | if (loadedEmbed != null) 50 | addField("Loaded", loadedEmbed.name) 51 | 52 | addField("Embeds", embedList) 53 | 54 | if (groups.isNotEmpty()) { 55 | groups.forEach { group -> 56 | addField(group.name, group.toString()) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | command("Limits") { 64 | description = "Display the discord embed limits." 65 | execute { 66 | it.respond { 67 | title { 68 | text = "Discord Limits" 69 | } 70 | description = "Below are all the limits imposed onto embeds by Discord." 71 | color = Color(0x00bfff) 72 | 73 | addInlineField("Total Character Limit", "$CHAR_LIMIT characters") 74 | addInlineField("Total Field Limit", "$FIELD_LIMIT fields") 75 | addInlineField("Title", "$TITLE_LIMIT characters") 76 | addInlineField("Description", "$DESCRIPTION_LIMIT characters") 77 | addInlineField("Field Name", "$FIELD_NAME_LIMIT characters") 78 | addInlineField("Field Value", "$FIELD_VALUE_LIMIT characters") 79 | addInlineField("Footer", "$FOOTER_LIMIT characters") 80 | addInlineField("Author", "$AUTHOR_LIMIT characters") 81 | addInlineField("", "[Discord Docs](https://discordapp.com/developers/docs/resources/channel#embed-limits-limits)") 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/commands/OwnerCommands.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.commands 2 | 3 | import me.jakejmattson.discordkt.api.annotations.CommandSet 4 | import me.jakejmattson.discordkt.api.arguments.* 5 | import me.jakejmattson.discordkt.api.dsl.command.commands 6 | import me.jakejmattson.discordkt.api.extensions.jda.sendPrivateMessage 7 | import me.jakejmattson.embedbot.arguments.EmbedArg 8 | import me.jakejmattson.embedbot.dataclasses.Configuration 9 | import me.jakejmattson.embedbot.extensions.* 10 | import me.jakejmattson.embedbot.services.* 11 | import kotlin.system.exitProcess 12 | 13 | @CommandSet("Owner") 14 | fun botConfigurationCommands(configuration: Configuration, embedService: EmbedService) = commands { 15 | command("Leave") { 16 | description = "Leave this guild and delete all associated information." 17 | requiredPermissionLevel = Permission.BOT_OWNER 18 | execute(GuildArg.makeOptional { it.guild!! }) { 19 | val guild = it.args.first 20 | 21 | configuration.guildConfigurations.remove(guild.idLong) 22 | configuration.save() 23 | 24 | val removedEmbeds = embedService.removeAllFromGuild(guild) 25 | val removedGroups = guild.getGuildEmbeds().groupList.size 26 | 27 | it.respond("Deleted all ($removedEmbeds) embeds." + 28 | "\nDeleted all ($removedGroups) groups." + 29 | "\nDeleted guild configuration for `${guild.name}`." + 30 | "\nLeaving guild. Goodbye.") 31 | 32 | guild.leave().queue() 33 | it.reactSuccess() 34 | } 35 | } 36 | 37 | command("Kill") { 38 | description = "Kill the bot. It will remember this decision." 39 | requiredPermissionLevel = Permission.BOT_OWNER 40 | execute { 41 | it.respond("Goodbye :(") 42 | exitProcess(0) 43 | } 44 | } 45 | 46 | command("Broadcast") { 47 | description = "Send a direct message to all guild owners." 48 | requiredPermissionLevel = Permission.BOT_OWNER 49 | execute(EveryArg("Message")) { 50 | val message = it.args.first 51 | 52 | it.discord.jda.guilds 53 | .mapNotNull { it.retrieveOwner().complete() } 54 | .distinctBy { it.id } 55 | .forEach { 56 | it.user.sendPrivateMessage(message) 57 | } 58 | 59 | it.reactSuccess() 60 | } 61 | } 62 | 63 | command("Transfer") { 64 | description = "Send an embed to another guild." 65 | requiredPermissionLevel = Permission.BOT_OWNER 66 | execute(EmbedArg("Embed").makeNullableOptional { it.guild?.getLoadedEmbed() }, 67 | GuildArg("Target Guild"), 68 | AnyArg("New Name").makeNullableOptional { null }) { 69 | 70 | val (embed, target, newName) = it.args 71 | 72 | embed ?: return@execute it.respond("No embed selected or loaded.") 73 | 74 | embed.name = newName.takeUnless { it.isNullOrBlank() } ?: embed.name 75 | 76 | val hasCollision = target.getGuildEmbeds().embedList.any { it.name == embed.name } 77 | 78 | if (hasCollision) 79 | return@execute it.respond("An embed with this name already exists in ${target.name}") 80 | 81 | embedService.addEmbed(target, embed) 82 | it.reactSuccess() 83 | } 84 | } 85 | 86 | command("Guilds") { 87 | description = "Get a complete list of guilds and info." 88 | requiredPermissionLevel = Permission.BOT_OWNER 89 | execute(ChoiceArg("Sort", "name", "size").makeOptional("name")) { 90 | val guilds = it.discord.jda.guilds 91 | val sortStyle = it.args.first.toLowerCase() 92 | 93 | val data = when (sortStyle) { 94 | "name" -> guilds.sortedBy { it.name } 95 | else -> guilds.sortedBy { -it.getGuildEmbeds().size } 96 | }.map { 97 | it.getGuildEmbeds().size to it.name 98 | } 99 | 100 | val formatter = "%${data.maxOf { it.first.toString().length }}s | %s" 101 | 102 | val report = data.joinToString("\n") { (size, name) -> 103 | formatter.format(size, name) 104 | } 105 | 106 | it.respond("Total Guilds: ${guilds.size}\n```$report```") 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/commands/UtilityCommands.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.commands 2 | 3 | import me.jakejmattson.discordkt.api.annotations.CommandSet 4 | import me.jakejmattson.discordkt.api.dsl.command.commands 5 | import me.jakejmattson.discordkt.api.extensions.stdlib.toTimeString 6 | import java.awt.Color 7 | import java.util.* 8 | 9 | private val startTime = Date() 10 | 11 | @CommandSet("Utility") 12 | fun utilityCommands() = commands { 13 | command("Status", "Ping", "Uptime") { 14 | description = "Display network status and total uptime." 15 | execute { event -> 16 | val jda = event.discord.jda 17 | 18 | jda.restPing.queue { restPing -> 19 | event.respond { 20 | color = Color(0x00bfff) 21 | 22 | val seconds = (Date().time - startTime.time) / 1000 23 | 24 | addField("Rest ping", "${restPing}ms") 25 | addField("Gateway ping", "${jda.gatewayPing}ms") 26 | addField("Total Uptime", seconds.toTimeString()) 27 | } 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/dataclasses/Cluster.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.dataclasses 2 | 3 | import me.jakejmattson.embedbot.extensions.removeEmbed 4 | import me.jakejmattson.embedbot.services.saveEmbeds 5 | import net.dv8tion.jda.api.entities.Guild 6 | 7 | data class Group(var name: String, val embeds: MutableList = mutableListOf()) { 8 | val size: Int 9 | get() = embeds.size 10 | 11 | fun getEmbedByName(name: String) = embeds.firstOrNull { it.name.toLowerCase() == name.toLowerCase() } 12 | fun removeEmbed(embed: Embed) = embeds.remove(embed) 13 | 14 | fun addEmbed(guild: Guild, embed: Embed, index: Int = -1) { 15 | guild.removeEmbed(embed) 16 | 17 | if (index == -1) 18 | embeds.add(embed) 19 | else 20 | embeds.add(index, embed) 21 | 22 | saveEmbeds() 23 | } 24 | 25 | override fun toString() = embeds.joinToString { it.name }.takeIf { it.isNotEmpty() } ?: "" 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/dataclasses/Configuration.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.dataclasses 2 | 3 | import me.jakejmattson.discordkt.api.dsl.data.Data 4 | import me.jakejmattson.discordkt.api.dsl.embed.embed 5 | import me.jakejmattson.discordkt.api.extensions.jda.* 6 | import net.dv8tion.jda.api.JDA 7 | import net.dv8tion.jda.api.entities.Guild 8 | 9 | data class Configuration(val botOwner: Long = 254786431656919051, 10 | val guildConfigurations: MutableMap = mutableMapOf()) : Data("config/config.json") { 11 | operator fun get(id: Long) = guildConfigurations[id] 12 | 13 | fun setup(guild: Guild) { 14 | if (guildConfigurations[guild.idLong] != null) return 15 | 16 | val everyone = guild.publicRole 17 | val newConfiguration = GuildConfiguration("embed!", everyone.idLong) 18 | 19 | guildConfigurations[guild.idLong] = newConfiguration 20 | save() 21 | 22 | guild.retrieveOwner().queue { 23 | it.user.sendPrivateMessage( 24 | embed { 25 | simpleTitle = guild.name 26 | description = "Thanks for using EmbedBot!" 27 | color = infoColor 28 | 29 | author { 30 | val me = guild.jda.retrieveUserById(254786431656919051).complete() 31 | 32 | name = me.fullName() 33 | iconUrl = me.effectiveAvatarUrl 34 | } 35 | 36 | val prefix = newConfiguration.prefix 37 | 38 | field { 39 | name = "Prefix" 40 | value = "This is the prefix used before commands given to the bot.\n" + 41 | "Default: $prefix\n" + 42 | "Use `${prefix}SetPrefix` to change this." 43 | } 44 | 45 | field { 46 | name = "Role" 47 | value = "This is the role that a user must have to use this bot.\n" + 48 | "Default: ${everyone.asMention}\n" + 49 | "Use `${prefix}SetRole` to change this." 50 | } 51 | } 52 | ) 53 | } 54 | } 55 | } 56 | 57 | data class GuildConfiguration(var prefix: String, 58 | var requiredRoleId: Long) { 59 | fun getLiveRole(jda: JDA) = jda.getRoleById(requiredRoleId) 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/dataclasses/Embed.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.dataclasses 2 | 3 | import me.jakejmattson.discordkt.api.Discord 4 | import me.jakejmattson.embedbot.extensions.* 5 | import me.jakejmattson.embedbot.services.* 6 | import me.jakejmattson.embedbot.utils.messages 7 | import net.dv8tion.jda.api.EmbedBuilder 8 | import net.dv8tion.jda.api.entities.Guild 9 | import java.awt.Color 10 | import java.time.temporal.TemporalAccessor 11 | import kotlin.streams.toList 12 | 13 | private fun EmbedBuilder.save(): EmbedBuilder { 14 | saveEmbeds() 15 | return this 16 | } 17 | 18 | data class Location(val channelId: String, val messageId: String) { 19 | override fun toString() = "Channel ID: $channelId\nMessage ID: $messageId" 20 | } 21 | 22 | data class Embed(var name: String, 23 | private val builder: EmbedBuilder = EmbedBuilder(), 24 | var location: Location? = null) { 25 | 26 | private val fields: MutableList 27 | get() = builder.fields 28 | 29 | val isEmpty: Boolean 30 | get() = builder.isEmpty 31 | 32 | val fieldCount: Int 33 | get() = fields.size 34 | 35 | val charCount: Int 36 | get() = if (!isEmpty) build().length else 0 37 | 38 | val locationString: String 39 | get() = location?.toString() ?: "" 40 | 41 | fun setAuthor(name: String, iconUrl: String) = builder.setAuthor(name, null, iconUrl).save() 42 | fun setColor(color: Color) = builder.setColor(color).save() 43 | fun setDescription(description: String) = builder.setDescription(description).save() 44 | fun setFooter(text: String, iconUrl: String) = builder.setFooter(text, iconUrl).save() 45 | fun setImage(url: String) = builder.setImage(url).save() 46 | fun setThumbnail(url: String) = builder.setThumbnail(url).save() 47 | fun setTimestamp(time: TemporalAccessor) = builder.setTimestamp(time).save() 48 | fun setTitle(title: String) = builder.setTitle(title).save() 49 | 50 | fun clearAuthor() = builder.setAuthor(null).save() 51 | fun clearColor() = builder.setColor(null).save() 52 | fun clearDescription() = builder.setDescription(null).save() 53 | fun clearFooter() = builder.setFooter(null, null).save() 54 | fun clearImage() = builder.setImage(null).save() 55 | fun clearThumbnail() = builder.setThumbnail(null).save() 56 | fun clearTimestamp() = builder.setTimestamp(null).save() 57 | fun clearTitle() = builder.setTitle(null).save() 58 | 59 | private fun setFields(fields: List) = clearFields().also { fields.forEach { builder.addField(it) } } 60 | fun setField(index: Int, field: Field) { 61 | fields[index] = field 62 | } 63 | 64 | fun addField(field: Field) = builder.addField(field) 65 | fun addBlankField(isInline: Boolean) = builder.addBlankField(isInline) 66 | fun removeField(index: Int) = fields.removeAt(index) 67 | fun insertField(index: Int, field: Field) = fields.add(index, field) 68 | fun setFieldName(index: Int, name: String) = with(fields[index]) { setField(index, Field(name, value, isInline)) } 69 | fun setFieldText(index: Int, value: String) = with(fields[index]) { setField(index, Field(name, value, isInline)) } 70 | fun setFieldInline(index: Int, isInline: Boolean) = with(fields[index]) { setField(index, Field(name, value, isInline)) } 71 | 72 | fun clear() = builder.clear().save() 73 | fun clearFields() = builder.clearFields().save() 74 | fun clearNonFields() { 75 | val fields = fields.stream().toList() 76 | clear() 77 | setFields(fields) 78 | } 79 | 80 | fun build() = builder.build() 81 | fun toJson() = gson.toJson(builder)!! 82 | 83 | fun isLoaded(guild: Guild) = guild.getLoadedEmbed() == this 84 | 85 | fun update(discord: Discord, channelId: String, messageId: String): OperationResult { 86 | val channel = discord.jda.getTextChannelById(channelId) 87 | ?: return false withMessage "Target channel does not exist." 88 | 89 | val message = discord.retrieveEntity { 90 | channel.retrieveMessageById(messageId).complete() 91 | } ?: return false withMessage "Target message does not exist." 92 | 93 | if (message.author != discord.jda.selfUser) 94 | return false withMessage "Target message is not from this bot." 95 | 96 | if (isEmpty) 97 | return false withMessage messages.EMPTY_EMBED 98 | 99 | val currentEmbed = message.getEmbed() 100 | ?: return false withMessage "Target message has no embed." 101 | 102 | if (currentEmbed == builder.build()) 103 | return false withMessage "This embed is up to date." 104 | 105 | message.editMessage(build()).queue() 106 | 107 | return true withMessage "Update successful." 108 | } 109 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/dataclasses/GuildEmbeds.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.dataclasses 2 | 3 | data class GuildEmbeds(var loadedEmbed: Embed? = null, val embedList: MutableList = mutableListOf(), val groupList: MutableList = mutableListOf()) { 4 | val size: Int 5 | get() = embedList.size + groupList.sumBy { it.size } 6 | 7 | fun addAndLoad(embed: Embed) { 8 | embedList.add(embed) 9 | load(embed) 10 | } 11 | 12 | fun load(embed: Embed) { 13 | loadedEmbed = embed 14 | } 15 | 16 | fun clear(): Int { 17 | val removed = embedList.size + groupList.sumBy { it.size } 18 | 19 | loadedEmbed = null 20 | embedList.clear() 21 | groupList.clear() 22 | 23 | return removed 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/dataclasses/OperationResult.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.dataclasses 2 | 3 | data class OperationResult(val wasSuccessful: Boolean, val message: String) 4 | 5 | infix fun Boolean.withMessage(message: String) = OperationResult(this, message) -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/extensions/Command.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.extensions 2 | 3 | import me.jakejmattson.discordkt.api.dsl.command.* 4 | import me.jakejmattson.embedbot.services.* 5 | import java.util.* 6 | 7 | fun CommandEvent<*>.reactSuccess() = message.addReaction("✅").queue() 8 | 9 | private object CommandPropertyStore { 10 | val requiresLoaded = WeakHashMap() 11 | val permissions = WeakHashMap() 12 | } 13 | 14 | var Command.requiresLoadedEmbed 15 | get() = CommandPropertyStore.requiresLoaded[this] ?: false 16 | set(value) { 17 | CommandPropertyStore.requiresLoaded[this] = value 18 | } 19 | 20 | var Command.requiredPermissionLevel: Permission 21 | get() = CommandPropertyStore.permissions[this] ?: DEFAULT_REQUIRED_PERMISSION 22 | set(value) { 23 | CommandPropertyStore.permissions[this] = value 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/extensions/Guild.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.extensions 2 | 3 | import me.jakejmattson.embedbot.dataclasses.Embed 4 | import me.jakejmattson.embedbot.services.getGuildEmbeds 5 | import net.dv8tion.jda.api.entities.Guild 6 | 7 | fun Guild.getEmbeds() = getGuildEmbeds().embedList.sortedBy { it.name } 8 | fun Guild.getGroups() = getGuildEmbeds().groupList.sortedBy { it.name } 9 | 10 | fun Guild.hasLoadedEmbed() = getLoadedEmbed() != null 11 | fun Guild.getLoadedEmbed() = getGuildEmbeds().loadedEmbed 12 | fun Guild.loadEmbed(embed: Embed) = getGuildEmbeds().load(embed) 13 | 14 | fun Guild.getEmbedByName(name: String): Embed? { 15 | val embed = getEmbeds().firstOrNull { it.name.toLowerCase() == name.toLowerCase() } 16 | 17 | if (embed != null) 18 | return embed 19 | 20 | getGroups().forEach { 21 | val embedInGroup = it.getEmbedByName(name) 22 | 23 | if (embedInGroup != null) 24 | return embedInGroup 25 | } 26 | 27 | return null 28 | } 29 | 30 | fun Guild.hasEmbedWithName(name: String) = getEmbedByName(name) != null 31 | 32 | fun Guild.getGroupByName(name: String) = getGroups().firstOrNull { it.name.toLowerCase() == name.toLowerCase() } 33 | fun Guild.hasGroupWithName(name: String) = getGroupByName(name) != null 34 | 35 | fun Guild.removeEmbed(embed: Embed) { 36 | if (embed.isLoaded(this)) 37 | getGuildEmbeds().loadedEmbed = null 38 | 39 | getGuildEmbeds().embedList.remove(embed) 40 | getGuildEmbeds().groupList.forEach { 41 | it.removeEmbed(embed) 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/extensions/Message.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.extensions 2 | 3 | import me.jakejmattson.discordkt.api.extensions.jda.containsURL 4 | import net.dv8tion.jda.api.entities.Message 5 | 6 | fun Message.getEmbed() = 7 | if (containsURL() || embeds.isEmpty()) 8 | null 9 | else 10 | embeds.first() -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/listeners/SetupListeners.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.listeners 2 | 3 | import com.google.common.eventbus.Subscribe 4 | import me.jakejmattson.embedbot.dataclasses.Configuration 5 | import net.dv8tion.jda.api.events.guild.GuildJoinEvent 6 | 7 | class SetupListeners(private val config: Configuration) { 8 | @Subscribe 9 | fun onJoin(event: GuildJoinEvent) = config.setup(event.guild) 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/preconditions/LoadedEmbedPrecondition.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.preconditions 2 | 3 | import me.jakejmattson.discordkt.api.dsl.command.CommandEvent 4 | import me.jakejmattson.discordkt.api.dsl.preconditions.* 5 | import me.jakejmattson.embedbot.extensions.* 6 | import me.jakejmattson.embedbot.utils.messages 7 | 8 | class LoadedEmbedPrecondition : Precondition() { 9 | override fun evaluate(event: CommandEvent<*>): PreconditionResult { 10 | val command = event.command ?: return Pass 11 | val guild = event.guild ?: return Fail(messages.MISSING_GUILD) 12 | 13 | if (command.requiresLoadedEmbed && !guild.hasLoadedEmbed()) 14 | return Fail(messages.MISSING_EMBED) 15 | 16 | return Pass 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/preconditions/PermissionPrecondition.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.preconditions 2 | 3 | import me.jakejmattson.discordkt.api.dsl.command.CommandEvent 4 | import me.jakejmattson.discordkt.api.dsl.preconditions.* 5 | import me.jakejmattson.discordkt.api.extensions.jda.toMember 6 | import me.jakejmattson.embedbot.extensions.requiredPermissionLevel 7 | import me.jakejmattson.embedbot.services.* 8 | 9 | class PermissionPrecondition(private val permissionsService: PermissionsService) : Precondition() { 10 | override fun evaluate(event: CommandEvent<*>): PreconditionResult { 11 | val command = event.command ?: return Fail() 12 | val requiredPermissionLevel = command.requiredPermissionLevel 13 | val guild = event.guild!! 14 | val member = event.author.toMember(guild)!! 15 | 16 | val response = when (requiredPermissionLevel) { 17 | Permission.BOT_OWNER -> "Missing clearance to use this command. You must be the bot owner." 18 | Permission.GUILD_OWNER -> "Missing clearance to use this command. You must be the guild owner." 19 | else -> "" 20 | } 21 | 22 | if (!permissionsService.hasClearance(member, requiredPermissionLevel)) 23 | return Fail(response) 24 | 25 | return Pass 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/services/EmbedService.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.services 2 | 3 | import com.google.gson.GsonBuilder 4 | import com.google.gson.reflect.TypeToken 5 | import me.jakejmattson.discordkt.api.annotations.Service 6 | import me.jakejmattson.embedbot.dataclasses.* 7 | import me.jakejmattson.embedbot.extensions.* 8 | import net.dv8tion.jda.api.EmbedBuilder 9 | import net.dv8tion.jda.api.entities.* 10 | import java.io.File 11 | 12 | typealias Field = MessageEmbed.Field 13 | 14 | val gson = GsonBuilder().setPrettyPrinting().create()!! 15 | 16 | private val embedFile = File("config/embeds.json") 17 | private val embedMap = loadEmbeds() 18 | 19 | fun Guild.getGuildEmbeds() = embedMap.getOrPut(this.id) { GuildEmbeds() } 20 | 21 | @Service 22 | class EmbedService { 23 | fun createEmbed(guild: Guild, name: String): Boolean { 24 | val embeds = guild.getGuildEmbeds() 25 | 26 | if (guild.hasEmbedWithName(name)) 27 | return false 28 | 29 | val newEmbed = Embed(name) 30 | embeds.addAndLoad(newEmbed) 31 | saveEmbeds() 32 | return true 33 | } 34 | 35 | fun addEmbed(guild: Guild, embed: Embed): Boolean { 36 | val embeds = guild.getGuildEmbeds() 37 | 38 | if (embed in embeds.embedList) 39 | return false 40 | 41 | embeds.addAndLoad(embed) 42 | saveEmbeds() 43 | return true 44 | } 45 | 46 | fun removeAllFromGuild(guild: Guild): Int { 47 | val embeds = guild.getGuildEmbeds() 48 | val removed = embeds.clear() 49 | embedMap.remove(guild.id) 50 | saveEmbeds() 51 | return removed 52 | } 53 | 54 | fun createGroup(guild: Guild, name: String) = 55 | if (!guild.hasGroupWithName(name)) { 56 | val groups = guild.getGuildEmbeds().groupList 57 | val newGroup = Group(name) 58 | 59 | groups.add(newGroup) 60 | saveEmbeds() 61 | newGroup 62 | } else 63 | null 64 | 65 | fun createGroupFromEmbeds(guild: Guild, group: Group): Boolean { 66 | val groups = guild.getGuildEmbeds().groupList 67 | 68 | if (guild.hasGroupWithName(group.name)) 69 | return false 70 | 71 | groups.add(group) 72 | saveEmbeds() 73 | return true 74 | } 75 | 76 | fun deleteGroup(guild: Guild, group: Group): Boolean { 77 | val groups = guild.getGuildEmbeds().groupList 78 | groups.remove(group) 79 | saveEmbeds() 80 | return true 81 | } 82 | 83 | fun removeEmbedFromGroup(guild: Guild, embed: Embed) { 84 | guild.removeEmbed(embed) 85 | addEmbed(guild, embed) 86 | saveEmbeds() 87 | } 88 | } 89 | 90 | fun saveEmbeds() = embedFile.writeText(gson.toJson(embedMap)) 91 | 92 | fun createEmbedFromJson(name: String, json: String) = Embed(name, gson.fromJson(json, EmbedBuilder::class.java)) 93 | 94 | private fun loadEmbeds() = 95 | if (embedFile.exists()) 96 | gson.fromJson(embedFile.readText(), object : TypeToken>() {}.type) 97 | else 98 | HashMap() -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/services/PermissionsService.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.services 2 | 3 | import me.jakejmattson.discordkt.api.annotations.Service 4 | import me.jakejmattson.embedbot.dataclasses.Configuration 5 | import net.dv8tion.jda.api.entities.Member 6 | 7 | enum class Permission { 8 | BOT_OWNER, 9 | GUILD_OWNER, 10 | USER, 11 | NONE 12 | } 13 | 14 | val DEFAULT_REQUIRED_PERMISSION = Permission.USER 15 | 16 | @Service 17 | class PermissionsService(private val configuration: Configuration) { 18 | fun hasClearance(member: Member, requiredPermissionLevel: Permission) = member.getPermissionLevel().ordinal <= requiredPermissionLevel.ordinal 19 | 20 | private fun Member.getPermissionLevel() = 21 | when { 22 | isBotOwner() -> Permission.BOT_OWNER 23 | isGuildOwner() -> Permission.GUILD_OWNER 24 | isUser() -> Permission.USER 25 | else -> Permission.NONE 26 | } 27 | 28 | private fun Member.isBotOwner() = user.idLong == configuration.botOwner 29 | private fun Member.isGuildOwner() = isOwner 30 | private fun Member.isUser(): Boolean { 31 | val role = configuration[guild.idLong]?.getLiveRole(guild.jda) 32 | return role?.isPublicRole == true || role in roles 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/jakejmattson/embedbot/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package me.jakejmattson.embedbot.utils 2 | 3 | import net.dv8tion.jda.api.entities.MessageEmbed 4 | 5 | const val CHAR_LIMIT = MessageEmbed.EMBED_MAX_LENGTH_BOT 6 | const val FIELD_LIMIT = 25 7 | const val TITLE_LIMIT = MessageEmbed.TITLE_MAX_LENGTH 8 | const val DESCRIPTION_LIMIT = MessageEmbed.TEXT_MAX_LENGTH 9 | const val FIELD_NAME_LIMIT = MessageEmbed.TITLE_MAX_LENGTH 10 | const val FIELD_VALUE_LIMIT = MessageEmbed.VALUE_MAX_LENGTH 11 | const val FOOTER_LIMIT = MessageEmbed.TEXT_MAX_LENGTH 12 | const val AUTHOR_LIMIT = MessageEmbed.TITLE_MAX_LENGTH 13 | 14 | val messages = Messages() 15 | 16 | class Messages( 17 | val MISSING_GUILD: String = "Must be invoked inside a guild.", 18 | val EMBED_ALREADY_EXISTS: String = "An embed with this name already exists.", 19 | val GROUP_ALREADY_EXISTS: String = "A group with this name already exists.", 20 | val NO_LOCATION: String = "This embed has no location set.", 21 | val MISSING_OPTIONAL_EMBED: String = "Please load an embed or specify one explicitly.", 22 | val MISSING_EMBED: String = "No embed loaded!", 23 | val EMPTY_EMBED: String = "Cannot build an empty embed." 24 | ) -------------------------------------------------------------------------------- /src/main/resources/templates/readme-template.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Kotlin ${kotlin} 4 | 5 | 6 | DiscordKt ${discordkt} 7 |
8 | 9 | Release 10 | 11 | 12 | Docker 13 | 14 |
15 | 16 | Discord JakeyWakey#1569 17 | 18 |

19 | 20 | # EmbedBot 21 | EmbedBot is a simple bot interface to create and modify embeds via commands. Creating embeds is not possible with a user account via the traditional Discord interface. You can create and store as many embeds as you want. These can be imported and exported as JSON for easy sharing between servers or persistent storage. You can also group embeds together, allowing you to deploy and manage multiple embeds at once. This will allow servers to easily set up a rules channel, keeping all of the related embeds grouped together and ready to send with a single command. 22 | 23 | ## Add it 24 | You can either [invite](https://discordapp.com/oauth2/authorize?client_id=439163847618592782&scope=bot&permissions=101440) it to your server or host it yourself with the [setup guide](setup.md). 25 | 26 | ## Using it 27 | To use the bot, see the [User Guide](guide.md) or a full list of [commands](commands.md). --------------------------------------------------------------------------------