├── .env.example ├── .github └── workflows │ ├── build.yml │ ├── discord-release.yml │ └── docker.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── build.gradle.kts ├── commands.md ├── docker-compose.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src └── main ├── kotlin └── me │ └── ddivad │ └── judgebot │ ├── Main.kt │ ├── arguments │ ├── AutocompleteArgs.kt │ ├── LowerMemberArg.kt │ └── LowerUserArg.kt │ ├── commands │ ├── AdminCommands.kt │ ├── ContextCommands.kt │ ├── GuildCommands.kt │ ├── InfractionCommands.kt │ ├── MessageCommands.kt │ ├── MuteCommands.kt │ ├── NoteCommands.kt │ ├── RuleCommands.kt │ ├── UserCommands.kt │ └── UtilityCommands.kt │ ├── conversations │ └── rules │ │ ├── AddRuleConversation.kt │ │ ├── ArchiveRuleConversation.kt │ │ └── EditRuleConversation.kt │ ├── dataclasses │ ├── Configuration.kt │ ├── GuildInformation.kt │ ├── GuildMember.kt │ ├── Infraction.kt │ ├── JoinLeave.kt │ ├── MessageDelete.kt │ ├── Meta.kt │ ├── Note.kt │ └── Permissions.kt │ ├── embeds │ ├── GuildEmbeds.kt │ ├── InfractionEmbeds.kt │ ├── MessageEmbeds.kt │ ├── RuleEmbeds.kt │ └── UserEmbeds.kt │ ├── extensions │ └── Kord.kt │ ├── listeners │ ├── JoinLeaveListener.kt │ ├── MemberReactionListeners.kt │ ├── NewChannelOverrideListener.kt │ ├── RejoinMuteListener.kt │ └── StaffReactionListeners.kt │ ├── preconditions │ ├── CommandLogger.kt │ └── PrefixPrecondition.kt │ ├── services │ ├── DatabaseService.kt │ ├── LoggingService.kt │ ├── database │ │ ├── Collection.kt │ │ ├── ConnectionService.kt │ │ ├── GuildOperations.kt │ │ ├── JoinLeaveOperations.kt │ │ ├── MessageDeleteOperations.kt │ │ ├── MetaOperations.kt │ │ ├── MigrationService.kt │ │ ├── UserOperations.kt │ │ └── migrations │ │ │ ├── v1.kt │ │ │ └── v2.kt │ └── infractions │ │ ├── BadPfpService.kt │ │ ├── BanService.kt │ │ ├── InfractionService.kt │ │ └── MuteService.kt │ └── util │ ├── MessageUtils.kt │ ├── NumberUtils.kt │ └── TimerUtils.kt └── resources ├── bot.properties └── logback.xml /.env.example: -------------------------------------------------------------------------------- 1 | BOT_TOKEN=discord-token-here 2 | DEFAULT_PREFIX=judge! 3 | MONGO_INITDB_ROOT_USERNAME= 4 | MONGO_INITDB_ROOT_PASSWORD= 5 | MONGO_INITDB_DATABASE=judgebot -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up JDK 15 | uses: actions/setup-java@v1 16 | with: 17 | java-version: 11 18 | - name: Cache local Maven repository 19 | uses: actions/cache@v2 20 | with: 21 | path: ~/.m2/repository 22 | key: maven-${{ hashFiles('build.gradle') }} 23 | restore-keys: maven- 24 | - name: Build 25 | run: ./gradlew build -------------------------------------------------------------------------------- /.github/workflows/discord-release.yml: -------------------------------------------------------------------------------- 1 | name: Post Release to Discord 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | run_main: 10 | runs-on: ubuntu-18.04 11 | name: Discord Webhook 12 | steps: 13 | - name: Send message 14 | uses: ddivad195/discord-styled-releases@main 15 | with: 16 | project_name: "Judgebot" 17 | embed_colour: "1315909" 18 | webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }} 19 | webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | docker: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: Checkout code 13 | uses: actions/checkout@v2 14 | - 15 | name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v1 17 | 18 | - 19 | name: Login to DockerHub 20 | uses: docker/login-action@v1 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} 24 | 25 | - 26 | name: Build and push 27 | uses: docker/build-push-action@v2 28 | with: 29 | context: . 30 | pull: true 31 | push: true 32 | tags: theprogrammershangout/judgebot:latest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ # 2 | .idea/ 3 | **.iml 4 | 5 | # Gradle # 6 | .gradle 7 | build 8 | 9 | # Project # 10 | data/ 11 | config/* 12 | .env 13 | 14 | src/main/resources/bot.properties -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM gradle:7.1-jdk16 AS build 3 | COPY --chown=gradle:gradle . /judgebot 4 | WORKDIR /judgebot 5 | RUN gradle shadowJar --no-daemon 6 | 7 | FROM openjdk:11.0.8-jre-slim 8 | RUN mkdir /config/ 9 | COPY --from=build /judgebot/build/libs/Judgebot.jar / 10 | 11 | ENTRYPOINT ["java", "-jar", "/Judgebot.jar"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 David 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 | Kotlin 1.5.20 2 | 3 | DiscordKt 0.22.0-SNAPSHOT 4 | 5 | 6 | Docker 7 | 8 | 9 | # Judgebot 10 | 11 | > A Discord bot to manage guild rules, infractions and punishments for guild members that don't play nicely. 12 | 13 | # Setup 14 | This bot can be setup and run locally using Docker and Docker-Compose. To start Judgebot locally, do the following: 15 | ``` 16 | $ cp .env.example .env 17 | ``` 18 | Edit the .env file with your favourite editor, filling out the token, default prefix and mongo configuration. 19 | 20 | ``` 21 | $ docker-compose up --detach 22 | ``` 23 | 24 | ## Author 25 | 👤 **Ddivad** 26 | * Github: [@ddivad195](https://github.com/ddivad195) 27 | 28 | ## 🤝 Contributing 29 | 30 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/the-programmers-hangout/JudgeBot/issues). 31 | 32 | ## Show your support 33 | 34 | Give a ⭐️ if this project helped you! 35 | 36 | ## 📝 License 37 | 38 | Copyright © 2020 [The Programmers Hangout](https://github.com/the-programmers-hangout).
39 | This project is [MIT](https://github.com/the-programmers-hangout/Taboo/blob/master/LICENSE) licensed. 40 | 41 | *** 42 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ 43 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.* 2 | 3 | group = "me.ddivad" 4 | version = "2.9.1" 5 | description = "A bot for managing discord infractions in an intelligent and user-friendly way." 6 | 7 | plugins { 8 | kotlin("jvm") version "1.6.0" 9 | kotlin("plugin.serialization") version "1.6.0" 10 | id("com.github.johnrengelman.shadow") version "7.0.0" 11 | } 12 | 13 | repositories { 14 | mavenCentral() 15 | maven("https://oss.sonatype.org/content/repositories/snapshots/") 16 | } 17 | 18 | dependencies { 19 | implementation("me.jakejmattson:DiscordKt:0.23.4") 20 | implementation("org.litote.kmongo:kmongo-coroutine:4.7.0") 21 | implementation("io.github.microutils:kotlin-logging-jvm:2.1.23") 22 | implementation("ch.qos.logback:logback-classic:1.4.0") 23 | implementation("ch.qos.logback:logback-core:1.4.0") 24 | 25 | } 26 | 27 | tasks { 28 | compileKotlin { 29 | kotlinOptions.jvmTarget = "1.8" 30 | 31 | Properties().apply { 32 | setProperty("name", "Judgebot") 33 | setProperty("description", project.description) 34 | setProperty("version", version.toString()) 35 | setProperty("url", "https://github.com/the-programmers-hangout/JudgeBot/") 36 | 37 | store(file("src/main/resources/bot.properties").outputStream(), null) 38 | } 39 | } 40 | 41 | shadowJar { 42 | archiveFileName.set("Judgebot.jar") 43 | manifest { 44 | attributes( 45 | "Main-Class" to "me.ddivad.judgebot.MainKt" 46 | ) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | ## Key 4 | | Symbol | Meaning | 5 | |-------------|--------------------------------| 6 | | [Argument] | Argument is not required. | 7 | | /Category | This is a subcommand group. | 8 | 9 | ## /BanReason 10 | | Commands | Arguments | Description | 11 | |----------|--------------|---------------------------------------------| 12 | | get | User | Get the ban reason for a user if it exists. | 13 | | set | User, Reason | Set a ban reason for a user. | 14 | 15 | ## /Configuration 16 | | Commands | Arguments | Description | 17 | |----------|------------------------------------|-----------------------------------------------------| 18 | | channel | ChannelType, Channel | Set the Alert or Logging channels | 19 | | reaction | Reaction, Emoji | Set the reactions used as various command shortcuts | 20 | | role | RoleType, Operation, Role | Add or remove configured roles | 21 | | setup | LogChannel, AlertChannel, MuteRole | Configure a guild to use Judgebot. | 22 | | view | | View guild configuration | 23 | 24 | ## /Message 25 | | Commands | Arguments | Description | 26 | |----------|-----------------|-----------------------------------------------------------------------------------------------| 27 | | remove | Member, ID | Remove a message record from a member. Only removes from history record, user DM will remain. | 28 | | send | Member, Content | Send an information message to a guild member | 29 | 30 | ## /Note 31 | | Commands | Arguments | Description | 32 | |----------|-------------------|----------------------------------------| 33 | | add | User, Content | Use this to add a note to a user. | 34 | | delete | User, ID | Use this to delete a note from a user. | 35 | | edit | User, ID, Content | Use this to edit a note. | 36 | 37 | ## /Rule 38 | | Commands | Arguments | Description | 39 | |----------|-----------|-------------------------------| 40 | | add | | Add a rule to this guild. | 41 | | archive | | Archive a rule in this guild. | 42 | | edit | Rule | Edit a rule in this guild. | 43 | 44 | ## Admin 45 | | Commands | Arguments | Description | 46 | |-------------------|----------------------|-------------------------------------------------------------| 47 | | activePunishments | | View active punishments for a guild. | 48 | | pointDecay | Option, LowerUserArg | Freeze point decay for a user | 49 | | removeInfraction | User, ID | Use this to delete (permanently) an infraction from a user. | 50 | | reset | LowerUserArg, choice | Reset a user's notes, infractions or whole record | 51 | 52 | ## Context 53 | | Commands | Arguments | Description | 54 | |----------------------|-----------|-------------------------------------------------------------------------------------------------| 55 | | contextMessageDelete | Message | Delete a message and notify a user via DM | 56 | | contextUserBadpfp | User | Apply a badpfp to a user (please use via the 'Apps' menu instead of as a command) | 57 | | contextUserHistory | User | View a condensed history for this user (please use via the 'Apps' menu instead of as a command) | 58 | | report | Message | Report a message to staff (please use via the 'Apps' menu instead of as a command) | 59 | 60 | ## Infraction 61 | | Commands | Arguments | Description | 62 | |----------|--------------------------------|------------------------------------------------------------------------------| 63 | | badname | Member | Rename a guild member that has a bad name. | 64 | | badpfp | Option, Member | Mutes a user and prompts them to change their pfp with a 30 minute ban timer | 65 | | strike | Member, Rule, Reason, [Weight] | Strike a user. | 66 | | warn | Member, Rule, Reason, [Force] | Warn a user. | 67 | 68 | ## Mute 69 | | Commands | Arguments | Description | 70 | |----------|----------------------|-----------------------------------| 71 | | gag | User | Mute a user for 5 minutes | 72 | | mute | Member, Time, Reason | Mute a user for a specified time. | 73 | | timeout | Member, Time | Time a user out | 74 | | unmute | Member | Unmute a user. | 75 | 76 | ## Rules 77 | | Commands | Arguments | Description | 78 | |-----------|-----------|---------------------------------------------------------------------------------------------------| 79 | | longRules | [Message] | List the rules (with descriptions) of this guild. Pass a message ID to edit existing rules embed. | 80 | | rules | [Message] | List the rules of this guild. Pass a message ID to edit existing rules embed. | 81 | | viewRule | Rule | List a rule from this guild. | 82 | 83 | ## User 84 | | Commands | Arguments | Description | 85 | |-----------------|------------------------------|-----------------------------------------------------------------| 86 | | alt | Option, Main, [Alt] | Link, Unlink or view a user's alt accounts | 87 | | ban | LowerUserArg, Reason, [Days] | Ban a member from this guild. | 88 | | deletedMessages | User | View a users messages deleted using the delete message reaction | 89 | | history | User | Use this to view a user's record. | 90 | | unban | User, [Thin-Ice] | Unban a banned member from this guild. | 91 | | whatpfp | User | Perform a reverse image search of a User's profile picture | 92 | 93 | ## Utility 94 | | Commands | Arguments | Description | 95 | |-------------|-----------|-------------------------------| 96 | | Help | [Command] | Display a help menu. | 97 | | info | | Bot info for Judgebot | 98 | | selfHistory | | View your infraction history. | 99 | 100 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | 4 | database: 5 | image: mongo 6 | restart: always 7 | container_name: judgebot-db 8 | env_file: 9 | - .env 10 | ports: 11 | - 27017:27017 12 | volumes: 13 | - judgebot:/data/db 14 | 15 | bot: 16 | container_name: judgebot 17 | image: theprogrammershangout/judgebot:latest 18 | volumes: 19 | - type: bind 20 | source: ./config/config.json 21 | target: /config/config.json 22 | restart: always 23 | env_file: 24 | - .env 25 | depends_on: 26 | - database 27 | 28 | volumes: 29 | judgebot: 30 | config: 31 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-programmers-hangout/JudgeBot/5d683df3166d72dd71cface54b0d2ed5c0bf5da9/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-7.2-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 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/Main.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot 2 | 3 | import dev.kord.common.annotation.KordPreview 4 | import dev.kord.gateway.Intent 5 | import dev.kord.gateway.Intents 6 | import dev.kord.gateway.PrivilegedIntent 7 | import me.ddivad.judgebot.dataclasses.Configuration 8 | import me.ddivad.judgebot.preconditions.logger 9 | import me.ddivad.judgebot.services.database.MigrationService 10 | import me.ddivad.judgebot.services.infractions.MuteService 11 | import me.jakejmattson.discordkt.dsl.bot 12 | import java.awt.Color 13 | 14 | @KordPreview 15 | @PrivilegedIntent 16 | suspend fun main() { 17 | val token = System.getenv("BOT_TOKEN") ?: null 18 | val defaultPrefix = System.getenv("DEFAULT_PREFIX") ?: "j!" 19 | 20 | require(token != null) { "Expected the bot token as an environment variable" } 21 | 22 | bot(token) { 23 | val configuration = data("config/config.json") { Configuration() } 24 | 25 | prefix { 26 | guild?.let { configuration[guild!!.id]?.prefix } ?: defaultPrefix 27 | } 28 | 29 | configure { 30 | commandReaction = null 31 | recommendCommands = false 32 | theme = Color.MAGENTA 33 | intents = Intents( 34 | Intent.Guilds, 35 | Intent.GuildBans, 36 | Intent.GuildMembers, 37 | Intent.DirectMessages, 38 | Intent.GuildMessageReactions, 39 | Intent.DirectMessagesReactions 40 | ) 41 | } 42 | 43 | onStart { 44 | val (muteService, migrationService) = this.getInjectionObjects(MuteService::class, MigrationService::class) 45 | try { 46 | migrationService.runMigrations() 47 | muteService.initGuilds() 48 | logger.info { "Bot Ready" } 49 | } catch (ex: Exception) { 50 | println(ex.message) 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/arguments/AutocompleteArgs.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.arguments 2 | 3 | import dev.kord.core.entity.interaction.GuildAutoCompleteInteraction 4 | import me.ddivad.judgebot.dataclasses.Configuration 5 | import me.ddivad.judgebot.services.DatabaseService 6 | import me.jakejmattson.discordkt.arguments.AnyArg 7 | 8 | fun autoCompletingRuleArg(databaseService: DatabaseService) = AnyArg("Rule", "Rule for infraction").autocomplete { 9 | val guild = (interaction as GuildAutoCompleteInteraction).getGuild() 10 | databaseService.guilds.getRulesForInfractionPrompt(guild) 11 | .map { "${it.number} - ${it.title}" } 12 | .filter { it.contains(input, true) } 13 | } 14 | 15 | fun autoCompletingWeightArg(configuration: Configuration) = AnyArg("Weight", "Strike Weight").autocomplete { 16 | val guild = (interaction as GuildAutoCompleteInteraction).getGuild() 17 | val guildConfig = configuration[guild.id] 18 | 1.rangeTo(guildConfig!!.infractionConfiguration.pointCeiling / 10).toList().map { it.toString() } 19 | .filter { it.contains(input, true) } 20 | }.optional("1") 21 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/arguments/LowerMemberArg.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.arguments 2 | 3 | import dev.kord.core.entity.Member 4 | import dev.kord.core.entity.User 5 | import me.ddivad.judgebot.dataclasses.Configuration 6 | import me.ddivad.judgebot.extensions.getHighestRolePosition 7 | import me.ddivad.judgebot.extensions.hasAdminRoles 8 | import me.ddivad.judgebot.extensions.hasStaffRoles 9 | import me.jakejmattson.discordkt.arguments.Error 10 | import me.jakejmattson.discordkt.arguments.Result 11 | import me.jakejmattson.discordkt.arguments.Success 12 | import me.jakejmattson.discordkt.arguments.UserArgument 13 | import me.jakejmattson.discordkt.commands.DiscordContext 14 | 15 | open class LowerMemberArg( 16 | override val name: String = "LowerMemberArg", 17 | override val description: String = "A Member with a lower rank", 18 | private val allowsBot: Boolean = false 19 | ) : UserArgument { 20 | companion object : LowerMemberArg() 21 | 22 | override suspend fun transform(input: User, context: DiscordContext): Result { 23 | val configuration = context.discord.getInjectionObjects(Configuration::class) 24 | val guild = context.guild ?: return Error("No guild found") 25 | val guildConfiguration = configuration[guild.id] ?: return Error("Guild not configured") 26 | val targetMember = input.asMemberOrNull(guild.id) ?: return Error("Member not found.") 27 | val authorAsMember = context.author.asMemberOrNull(guild.id) ?: return Error("Member not found.") 28 | 29 | if (!allowsBot && targetMember.isBot) 30 | return Error("Cannot be a bot") 31 | 32 | return if (authorAsMember.id == targetMember.id) { 33 | Error("Cannot run command on yourself") 34 | } else if (authorAsMember.hasAdminRoles(guildConfiguration) && !targetMember.hasAdminRoles(guildConfiguration)) { 35 | Success(targetMember) 36 | } else if (targetMember.hasStaffRoles(guildConfiguration)) { 37 | Error("Cannot run command on staff members") 38 | } else if (authorAsMember.getHighestRolePosition() > targetMember.getHighestRolePosition()) { 39 | Success(targetMember) 40 | } else Error("Missing permissions to target this member") 41 | } 42 | 43 | override suspend fun generateExamples(context: DiscordContext): List = listOf(context.author.mention) 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/arguments/LowerUserArg.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.arguments 2 | 3 | import dev.kord.core.entity.User 4 | import me.ddivad.judgebot.dataclasses.Configuration 5 | import me.ddivad.judgebot.extensions.getHighestRolePosition 6 | import me.ddivad.judgebot.extensions.hasAdminRoles 7 | import me.ddivad.judgebot.extensions.hasStaffRoles 8 | import me.jakejmattson.discordkt.arguments.Error 9 | import me.jakejmattson.discordkt.arguments.Result 10 | import me.jakejmattson.discordkt.arguments.Success 11 | import me.jakejmattson.discordkt.arguments.UserArgument 12 | import me.jakejmattson.discordkt.commands.DiscordContext 13 | 14 | open class LowerUserArg( 15 | override val name: String = "LowerUserArg", 16 | override val description: String = "A User with a lower rank", 17 | private val allowsBot: Boolean = false 18 | ) : UserArgument { 19 | companion object : LowerUserArg() 20 | 21 | override suspend fun transform(input: User, context: DiscordContext): Result { 22 | val configuration = context.discord.getInjectionObjects(Configuration::class) 23 | val guild = context.guild ?: return Error("No guild found") 24 | val guildConfiguration = configuration[guild.id] ?: return Error("Guild not configured") 25 | val targetMember = input.asMemberOrNull(guild.id) ?: return Success(input) 26 | val authorAsMember = context.author.asMemberOrNull(guild.id) ?: return Error("Member not found.") 27 | 28 | if (!allowsBot && targetMember.isBot) 29 | return Error("Cannot be a bot") 30 | 31 | return if (authorAsMember.id == targetMember.id) { 32 | Error("Cannot run command on yourself") 33 | } else if (authorAsMember.hasAdminRoles(guildConfiguration) && !targetMember.hasAdminRoles(guildConfiguration)) { 34 | Success(targetMember.asUser()) 35 | } else if (targetMember.hasStaffRoles(guildConfiguration)) { 36 | Error("Cannot run command on staff members") 37 | } else if (authorAsMember.getHighestRolePosition() > targetMember.getHighestRolePosition()) { 38 | Success(targetMember.asUser()) 39 | } else Error("Missing permissions to target this member") 40 | } 41 | 42 | override suspend fun generateExamples(context: DiscordContext): List = listOf(context.author.mention) 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/AdminCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import dev.kord.core.behavior.interaction.response.respond 4 | import dev.kord.rest.builder.message.modify.embed 5 | import me.ddivad.judgebot.arguments.LowerUserArg 6 | import me.ddivad.judgebot.dataclasses.Configuration 7 | import me.ddivad.judgebot.dataclasses.Permissions 8 | import me.ddivad.judgebot.embeds.createActivePunishmentsEmbed 9 | import me.ddivad.judgebot.services.DatabaseService 10 | import me.ddivad.judgebot.services.infractions.MuteService 11 | import me.jakejmattson.discordkt.arguments.ChoiceArg 12 | import me.jakejmattson.discordkt.arguments.IntegerArg 13 | import me.jakejmattson.discordkt.arguments.MemberArg 14 | import me.jakejmattson.discordkt.commands.commands 15 | import me.jakejmattson.discordkt.extensions.TimeStamp 16 | import java.time.Instant 17 | 18 | @Suppress("Unused") 19 | fun adminCommands(databaseService: DatabaseService, configuration: Configuration, muteService: MuteService) = 20 | commands("Admin", Permissions.ADMINISTRATOR) { 21 | slash( 22 | "removeInfraction", 23 | "Use this to delete (permanently) an infraction from a user.", 24 | Permissions.ADMINISTRATOR 25 | ) { 26 | execute(MemberArg("User", "Target User"), IntegerArg("ID", "Infraction ID")) { 27 | val user = databaseService.users.getOrCreateUser(args.first, guild) 28 | if (user.getGuildInfo(guild.id.toString()).infractions.isEmpty()) { 29 | respond("User has no infractions.") 30 | return@execute 31 | } 32 | databaseService.users.removeInfraction(guild, user, args.second) 33 | respond("Infraction removed.") 34 | } 35 | } 36 | 37 | slash("reset", "Reset a user's notes, infractions or whole record", Permissions.ADMINISTRATOR) { 38 | execute( 39 | LowerUserArg, 40 | ChoiceArg( 41 | "choice", 42 | "Part of the user record to reset", 43 | "Infractions", 44 | "Notes", 45 | "Info", 46 | "Point Decay", 47 | "All" 48 | ) 49 | ) { 50 | val (target, choice) = args 51 | val user = databaseService.users.getOrCreateUser(target, guild) 52 | when (choice) { 53 | "Infractions" -> { 54 | if (user.getGuildInfo(guild.id.toString()).infractions.isEmpty()) { 55 | respond("User has no infractions.") 56 | return@execute 57 | } 58 | databaseService.users.cleanseInfractions(guild, user) 59 | respondPublic("Infractions reset for ${target.mention}.") 60 | } 61 | "Notes" -> { 62 | if (user.getGuildInfo(guild.id.toString()).notes.isEmpty()) { 63 | respond("User has no notes.") 64 | return@execute 65 | } 66 | databaseService.users.cleanseNotes(guild, user) 67 | respondPublic("Notes reset for ${target.mention}.") 68 | } 69 | "Point Decay" -> { 70 | databaseService.users.updatePointDecayState(guild, user, false) 71 | respondPublic("Point decay reset for ${target.mention}.") 72 | } 73 | "All" -> { 74 | databaseService.users.resetUserRecord(guild, user) 75 | respondPublic("User ${target.mention} reset.") 76 | } 77 | } 78 | } 79 | } 80 | 81 | slash("activePunishments", "View active punishments for a guild.", Permissions.ADMINISTRATOR) { 82 | execute { 83 | val interactionResponse = interaction?.deferEphemeralResponse() ?: return@execute 84 | val punishments = databaseService.guilds.getActivePunishments(guild) 85 | if (punishments.isEmpty()) { 86 | interactionResponse.respond { content = "No active punishments found." } 87 | return@execute 88 | } 89 | interactionResponse.respond { embed { createActivePunishmentsEmbed(guild, punishments) } } 90 | } 91 | } 92 | 93 | slash("pointDecay", "Freeze point decay for a user", Permissions.ADMINISTRATOR) { 94 | execute( 95 | ChoiceArg("Option", "Freeze / Unfreeze point decay", "Freeze", "Unfreeze", "Thin Ice"), 96 | LowerUserArg 97 | ) { 98 | val (choice, user) = args 99 | var guildMember = databaseService.users.getOrCreateUser(user, guild) 100 | when (choice) { 101 | "Freeze" -> { 102 | databaseService.users.updatePointDecayState(guild, guildMember, true) 103 | respondPublic("Point decay **frozen** for ${user.mention}") 104 | } 105 | "Unfreeze" -> { 106 | databaseService.users.updatePointDecayState(guild, guildMember, false) 107 | respondPublic("Point decay **unfrozen** for ${user.mention}") 108 | } 109 | else -> { 110 | guildMember = databaseService.users.enableThinIceMode(guild, guildMember) 111 | respondPublic( 112 | "Point decay frozen and points set to 40 for ${user.mention}. Point decay will resume on ${ 113 | TimeStamp.at( 114 | Instant.ofEpochMilli(guildMember.getGuildInfo(guild.id.toString()).pointDecayTimer) 115 | ) 116 | }" 117 | ) 118 | } 119 | } 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/ContextCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import dev.kord.common.exception.RequestException 4 | import dev.kord.core.behavior.getChannelOf 5 | import dev.kord.core.behavior.interaction.response.respond 6 | import dev.kord.core.entity.channel.TextChannel 7 | import dev.kord.x.emoji.Emojis 8 | import dev.kord.x.emoji.addReaction 9 | import me.ddivad.judgebot.dataclasses.Configuration 10 | import me.ddivad.judgebot.dataclasses.Permissions 11 | import me.ddivad.judgebot.embeds.createCondensedHistoryEmbed 12 | import me.ddivad.judgebot.embeds.createMessageDeleteEmbed 13 | import me.ddivad.judgebot.embeds.createSelfHistoryEmbed 14 | import me.ddivad.judgebot.extensions.hasStaffRoles 15 | import me.ddivad.judgebot.services.DatabaseService 16 | import me.ddivad.judgebot.services.infractions.BadPfpService 17 | import me.ddivad.judgebot.services.infractions.InfractionService 18 | import me.ddivad.judgebot.util.createFlagMessage 19 | import me.jakejmattson.discordkt.commands.commands 20 | import me.jakejmattson.discordkt.extensions.sendPrivateMessage 21 | 22 | @Suppress("Unused") 23 | fun contextCommands( 24 | configuration: Configuration, 25 | databaseService: DatabaseService, 26 | badPfpService: BadPfpService, 27 | infractionService: InfractionService 28 | ) = 29 | commands("Context") { 30 | message( 31 | "Report Message", 32 | "report", 33 | "Report a message to staff (please use via the 'Apps' menu instead of as a command)" 34 | ) { 35 | val guildConfiguration = configuration[guild.asGuild().id] ?: return@message 36 | guild.getChannelOf(guildConfiguration.loggingConfiguration.alertChannel) 37 | .createMessage(createFlagMessage(author, args.first, channel)) 38 | .addReaction(Emojis.question) 39 | respond("Message flagged successfully, thanks!") 40 | } 41 | 42 | user( 43 | "History", 44 | "contextUserHistory", 45 | "View a condensed history for this user (please use via the 'Apps' menu instead of as a command)", 46 | Permissions.EVERYONE 47 | ) { 48 | val targetMember = arg.asMemberOrNull(guild.id) ?: return@user 49 | val guildConfiguration = configuration[guild.id] ?: return@user 50 | if (author.asMember(guild.id).hasStaffRoles(guildConfiguration)) { 51 | respond("Record for ${arg.mention}") { 52 | createCondensedHistoryEmbed( 53 | arg, 54 | databaseService.users.getOrCreateUser(arg, guild.asGuild()), 55 | guild.asGuild(), 56 | configuration 57 | ) 58 | } 59 | } else if (author.id == targetMember.id) { 60 | respond { 61 | createSelfHistoryEmbed( 62 | arg, 63 | databaseService.users.getOrCreateUser(arg, guild.asGuild()), 64 | guild.asGuild(), 65 | configuration 66 | ) 67 | } 68 | } else respond("Missing required permissions") 69 | } 70 | 71 | user( 72 | "BadPFP", 73 | "contextUserBadpfp", 74 | "Apply a badpfp to a user (please use via the 'Apps' menu instead of as a command)", 75 | Permissions.STAFF 76 | ) { 77 | val targetMember = arg.asMemberOrNull(guild.id) 78 | val interactionResponse = interaction!!.deferEphemeralResponse() 79 | if (targetMember == null) { 80 | respond("Member ${arg.mention} is no longer in this guild") 81 | return@user 82 | } 83 | badPfpService.applyBadPfp(targetMember, guild) 84 | interactionResponse.respond { 85 | content = "${targetMember.mention} has been muted and a badpfp has been triggered." 86 | } 87 | } 88 | 89 | message( 90 | "Delete Message", 91 | "contextMessageDelete", 92 | "Delete a message and notify a user via DM", 93 | Permissions.STAFF 94 | ) { 95 | val targetMember = arg.getAuthorAsMember() ?: return@message 96 | val interactionResponse = interaction!!.deferEphemeralResponse() 97 | infractionService.deleteMessage(guild, targetMember, arg, author.asMember(guild.id)) 98 | try { 99 | targetMember.sendPrivateMessage { 100 | createMessageDeleteEmbed(guild, arg) 101 | } 102 | interactionResponse.respond { content = "Message deleted" } 103 | } catch (ex: RequestException) { 104 | interactionResponse.respond { 105 | content = "User ${targetMember.mention} has DM's disabled. Message deleted without notification." 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/GuildCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import dev.kord.core.behavior.interaction.response.respond 4 | import me.ddivad.judgebot.dataclasses.Configuration 5 | import me.ddivad.judgebot.dataclasses.GuildConfiguration 6 | import me.ddivad.judgebot.dataclasses.LoggingConfiguration 7 | import me.ddivad.judgebot.dataclasses.Permissions 8 | import me.ddivad.judgebot.embeds.createConfigEmbed 9 | import me.ddivad.judgebot.services.DatabaseService 10 | import me.ddivad.judgebot.services.infractions.MuteService 11 | import me.jakejmattson.discordkt.arguments.* 12 | import me.jakejmattson.discordkt.commands.subcommand 13 | 14 | @Suppress("unused") 15 | fun configurationSubcommands(configuration: Configuration, databaseService: DatabaseService, muteService: MuteService) = subcommand("Configuration", Permissions.ADMINISTRATOR) { 16 | sub("setup", "Configure a guild to use Judgebot.") { 17 | execute(ChannelArg("LogChannel"), ChannelArg("AlertChannel"), RoleArg("MuteRole")) { 18 | val (logChannel, alertChannel, mutedRole) = args 19 | val interactionResponse = interaction?.deferPublicResponse() ?: return@execute 20 | val newConfiguration = GuildConfiguration(loggingConfiguration = LoggingConfiguration(alertChannel.id, logChannel.id), mutedRole = mutedRole.id) 21 | databaseService.guilds.setupGuild(guild) 22 | configuration.setup(guild, newConfiguration) 23 | muteService.setupMutedRole(guild) 24 | interactionResponse.respond { content = "Guild setup" } 25 | } 26 | } 27 | 28 | sub("role", "Add or remove configured roles") { 29 | execute( 30 | ChoiceArg("RoleType", "Role type to set", "Admin", "Staff", "Moderator"), 31 | ChoiceArg("Operation", "Add / Remove", "Add", "Remove"), 32 | RoleArg 33 | ) { 34 | val (roleType, operation, role) = args 35 | val guildConfiguration = configuration[guild.id] ?: return@execute 36 | 37 | when (roleType) { 38 | "Admin" -> { 39 | if (operation == "Add") { 40 | guildConfiguration.adminRoles.add(role.id) 41 | respondPublic("Added **${role.name}** to Admin roles.") 42 | } else { 43 | if (!guildConfiguration.adminRoles.contains(role.id)) { 44 | respond("**${role.name}** is not a configured Admin rol") 45 | } else { 46 | guildConfiguration.adminRoles.remove(role.id) 47 | respondPublic("Removed **${role.name}** from Admin Roles") 48 | } 49 | } 50 | } 51 | "Staff" -> { 52 | if (operation == "Add") { 53 | guildConfiguration.staffRoles.add(role.id) 54 | respondPublic("Added **${role.name}** to Staff roles.") 55 | } else { 56 | if (!guildConfiguration.staffRoles.contains(role.id)) { 57 | respond("**${role.name}** is not a configured Staff role") 58 | } else { 59 | guildConfiguration.staffRoles.remove(role.id) 60 | respondPublic("Removed **${role.name}** from Staff Roles") 61 | } 62 | } 63 | } 64 | "Moderator" -> { 65 | if (operation == "Add") { 66 | guildConfiguration.moderatorRoles.add(role.id) 67 | respondPublic("Added **${role.name}** to Moderator roles.") 68 | } else { 69 | if (!guildConfiguration.moderatorRoles.contains(role.id)) { 70 | respond("**${role.name}** is not a configured Moderator role") 71 | } else { 72 | guildConfiguration.moderatorRoles.remove(role.id) 73 | respondPublic("Removed **${role.name}** from Moderator Roles") 74 | } 75 | } 76 | } 77 | } 78 | configuration.save() 79 | } 80 | } 81 | 82 | sub("channel", "Set the Alert or Logging channels") { 83 | execute(ChoiceArg("ChannelType", "Channel type to modify", "Logging", "Alert"), ChannelArg("Channel")) { 84 | val (channelType, channel) = args 85 | val guildConfiguration = configuration[guild.id] ?: return@execute 86 | when (channelType) { 87 | "Logging" -> { 88 | guildConfiguration.loggingConfiguration.loggingChannel = channel.id 89 | } 90 | "Alert" -> { 91 | guildConfiguration.loggingConfiguration.alertChannel = channel.id 92 | } 93 | } 94 | configuration.save() 95 | respondPublic("Channel set to ${channel.mention}") 96 | } 97 | } 98 | 99 | sub("reaction", "Set the reactions used as various command shortcuts") { 100 | execute( 101 | ChoiceArg("Reaction", "Choose the reaction to set", "Flag Message", "Gag", "Delete Message"), 102 | UnicodeEmojiArg() 103 | ) { 104 | val (reactionType, reaction) = args 105 | val guildConfiguration = configuration[guild.id] ?: return@execute 106 | when (reactionType) { 107 | "Gag" -> { 108 | guildConfiguration.reactions.gagReaction = reaction.unicode 109 | } 110 | "Flag Message" -> { 111 | guildConfiguration.reactions.flagMessageReaction = reaction.unicode 112 | } 113 | "Delete Message" -> { 114 | guildConfiguration.reactions.deleteMessageReaction = reaction.unicode 115 | } 116 | } 117 | configuration.save() 118 | respondPublic("Reaction set to ${reaction.unicode}") 119 | } 120 | } 121 | 122 | sub("view", "View guild configuration") { 123 | execute { 124 | val guildConfiguration = configuration[guild.id] ?: return@execute 125 | respondPublic { 126 | createConfigEmbed(guildConfiguration, guild) 127 | } 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/InfractionCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import dev.kord.common.annotation.KordPreview 4 | import dev.kord.common.exception.RequestException 5 | import dev.kord.core.behavior.interaction.response.respond 6 | import me.ddivad.judgebot.arguments.LowerMemberArg 7 | import me.ddivad.judgebot.arguments.autoCompletingRuleArg 8 | import me.ddivad.judgebot.arguments.autoCompletingWeightArg 9 | import me.ddivad.judgebot.dataclasses.Configuration 10 | import me.ddivad.judgebot.dataclasses.Infraction 11 | import me.ddivad.judgebot.dataclasses.InfractionType 12 | import me.ddivad.judgebot.dataclasses.Permissions 13 | import me.ddivad.judgebot.embeds.createHistoryEmbed 14 | import me.ddivad.judgebot.extensions.testDmStatus 15 | import me.ddivad.judgebot.services.DatabaseService 16 | import me.ddivad.judgebot.services.infractions.BadPfpService 17 | import me.ddivad.judgebot.services.infractions.InfractionService 18 | import me.jakejmattson.discordkt.arguments.AnyArg 19 | import me.jakejmattson.discordkt.arguments.BooleanArg 20 | import me.jakejmattson.discordkt.arguments.ChoiceArg 21 | import me.jakejmattson.discordkt.commands.commands 22 | import me.jakejmattson.discordkt.extensions.createMenu 23 | 24 | @KordPreview 25 | @Suppress("unused") 26 | fun createInfractionCommands( 27 | databaseService: DatabaseService, 28 | config: Configuration, 29 | infractionService: InfractionService, 30 | badPfpService: BadPfpService 31 | ) = commands("Infraction") { 32 | slash("warn", "Warn a user.", Permissions.MODERATOR) { 33 | execute( 34 | LowerMemberArg("Member", "Target Member"), 35 | autoCompletingRuleArg(databaseService), 36 | AnyArg("Reason", "Infraction reason to send to user"), 37 | BooleanArg( 38 | "Force", 39 | "true", 40 | "false", 41 | "Override recommendation to strike user instead. Please discuss with staff before using this option" 42 | ).optional(false) 43 | ) { 44 | val (targetMember, ruleName, reason, force) = args 45 | val guildConfiguration = config[guild.id] ?: return@execute 46 | val interactionResponse = interaction?.deferPublicResponse() ?: return@execute 47 | val user = databaseService.users.getOrCreateUser(targetMember, guild) 48 | if (user.getTotalHistoricalPoints(guild) >= guildConfiguration.infractionConfiguration.warnUpgradeThreshold && !force) { 49 | interactionResponse.respond { 50 | content = 51 | "This user has more than ${guildConfiguration.infractionConfiguration.warnUpgradeThreshold} historical points, so please consider striking (`/strike`) instead. You can use the optional `Force` argument to override this, but please discuss with other staff before doing so." 52 | } 53 | return@execute 54 | } 55 | val infraction = Infraction( 56 | author.id.toString(), 57 | reason, 58 | InfractionType.Warn, 59 | guildConfiguration.infractionConfiguration.warnPoints, 60 | getRuleNumber(ruleName) 61 | ) 62 | infractionService.infract(targetMember, guild, user, infraction) 63 | val dmEnabled: Boolean = try { 64 | targetMember.testDmStatus() 65 | true 66 | } catch (ex: RequestException) { 67 | false 68 | } 69 | channel.createMenu { createHistoryEmbed(targetMember, user, guild, config, databaseService) } 70 | interactionResponse.respond { 71 | content = 72 | "Updated history for ${targetMember.mention}: ${if (!dmEnabled) "\n**Note**: User has DMs disabled" else ""}" 73 | } 74 | } 75 | } 76 | 77 | slash("strike", "Strike a user.", Permissions.STAFF) { 78 | execute( 79 | LowerMemberArg("Member", "Target Member"), 80 | autoCompletingRuleArg(databaseService), 81 | AnyArg("Reason", "Infraction reason to send to user"), 82 | autoCompletingWeightArg(config) 83 | ) { 84 | val (targetMember, ruleName, reason, weight) = args 85 | val guildConfiguration = config[guild.id] ?: return@execute 86 | val interactionResponse = interaction?.deferPublicResponse() ?: return@execute 87 | val user = databaseService.users.getOrCreateUser(targetMember, guild) 88 | val infraction = Infraction( 89 | author.id.toString(), 90 | reason, 91 | InfractionType.Strike, 92 | weight.toInt() * guildConfiguration.infractionConfiguration.strikePoints, 93 | getRuleNumber(ruleName) 94 | ) 95 | infractionService.infract(targetMember, guild, user, infraction) 96 | val dmEnabled: Boolean = try { 97 | targetMember.testDmStatus() 98 | true 99 | } catch (ex: RequestException) { 100 | false 101 | } 102 | channel.createMenu { createHistoryEmbed(targetMember, user, guild, config, databaseService) } 103 | interactionResponse.respond { 104 | content = 105 | "Updated history for ${targetMember.mention}: ${if (!dmEnabled) "\n**Note**: User has DMs disabled" else ""}" 106 | } 107 | } 108 | } 109 | 110 | slash("badpfp", "Mutes a user and prompts them to change their pfp with a 30 minute ban timer", Permissions.STAFF) { 111 | execute( 112 | ChoiceArg("Option", "Trigger or cancel badpfp for a user", "Trigger", "Cancel"), 113 | LowerMemberArg("Member", "Target Member") 114 | ) { 115 | val (option, targetMember) = args 116 | val interactionResponse = interaction!!.deferPublicResponse() 117 | var dmEnabled: Boolean 118 | try { 119 | targetMember.testDmStatus() 120 | dmEnabled = true 121 | } catch (ex: RequestException) { 122 | dmEnabled = false 123 | interactionResponse.respond { 124 | content = "${targetMember.mention} has DMs disabled. No messages will be sent." 125 | } 126 | } 127 | if (option == "Cancel") { 128 | when (badPfpService.hasActiveBapPfp(targetMember)) { 129 | true -> { 130 | badPfpService.cancelBadPfp(guild, targetMember) 131 | interactionResponse.respond { content = "Badpfp cancelled for ${targetMember.mention}" } 132 | } 133 | false -> interactionResponse.respond { 134 | content = "${targetMember.mention} does not have an active badpfp." 135 | } 136 | } 137 | return@execute 138 | } 139 | 140 | val badPfp = Infraction(author.id.toString(), "BadPfp", InfractionType.BadPfp) 141 | badPfpService.applyBadPfp(targetMember, guild) 142 | databaseService.users.addInfraction( 143 | guild, 144 | databaseService.users.getOrCreateUser(targetMember, guild), 145 | badPfp 146 | ) 147 | interactionResponse.respond { 148 | content = "${targetMember.mention} has been muted and a badpfp has been triggered." 149 | } 150 | } 151 | } 152 | 153 | slash("badname", "Rename a guild member that has a bad name.", Permissions.MODERATOR) { 154 | execute(LowerMemberArg("Member", "Target Member")) { 155 | infractionService.badName(args.first) 156 | respond("User renamed to ${args.first.mention}") 157 | } 158 | } 159 | } 160 | 161 | private fun getRuleNumber(ruleName: String): Int? { 162 | return if (ruleName.split(" -").first().toInt() > 0) { 163 | ruleName.split(" -").first().toInt() 164 | } else null 165 | } 166 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/MessageCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import dev.kord.common.exception.RequestException 4 | import dev.kord.core.behavior.interaction.response.respond 5 | import dev.kord.rest.builder.message.modify.embed 6 | import dev.kord.x.emoji.Emojis 7 | import me.ddivad.judgebot.arguments.LowerMemberArg 8 | import me.ddivad.judgebot.dataclasses.Info 9 | import me.ddivad.judgebot.dataclasses.Permissions 10 | import me.ddivad.judgebot.embeds.createInformationEmbed 11 | import me.ddivad.judgebot.extensions.testDmStatus 12 | import me.ddivad.judgebot.services.DatabaseService 13 | import me.jakejmattson.discordkt.arguments.EveryArg 14 | import me.jakejmattson.discordkt.arguments.IntegerArg 15 | import me.jakejmattson.discordkt.arguments.MemberArg 16 | import me.jakejmattson.discordkt.commands.subcommand 17 | import me.jakejmattson.discordkt.extensions.sendPrivateMessage 18 | 19 | @Suppress("unused") 20 | fun createInformationCommands(databaseService: DatabaseService) = subcommand("Message", Permissions.STAFF) { 21 | sub("send", "Send an information message to a guild member") { 22 | execute(LowerMemberArg("Member", "Target Member"), EveryArg("Content")) { 23 | val (target, content) = args 24 | val interactionResponse = interaction?.deferPublicResponse() ?: return@execute 25 | var dmEnabled: Boolean 26 | try { 27 | target.testDmStatus() 28 | dmEnabled = true 29 | val user = databaseService.users.getOrCreateUser(target, guild) 30 | val information = Info(content, author.id.toString()) 31 | databaseService.users.addInfo(guild, user, information) 32 | target.sendPrivateMessage { 33 | createInformationEmbed(guild, target, information) 34 | } 35 | } catch (ex: RequestException) { 36 | dmEnabled = false 37 | } 38 | 39 | interactionResponse.respond { 40 | embed { 41 | color = discord.configuration.theme 42 | title = "Message Command: ${if (dmEnabled) Emojis.whiteCheckMark else Emojis.x}" 43 | description = if (dmEnabled) { 44 | "Message added and sent to ${target.mention}" 45 | } else { 46 | "User ${target.mention} has DMs disabled. Message not added or sent." 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | sub("remove", "Remove a message record from a member. Only removes from history record, user DM will remain.") { 54 | execute(MemberArg, IntegerArg("ID", "ID of message record to delete")) { 55 | val (target, id) = args 56 | val interactionResponse = interaction?.deferPublicResponse() ?: return@execute 57 | val user = databaseService.users.getOrCreateUser(target, guild) 58 | if (user.getGuildInfo(guild.id.toString()).info.isEmpty()) { 59 | respond("${target.mention} has no message records.") 60 | return@execute 61 | } 62 | databaseService.users.removeInfo(guild, user, id) 63 | interactionResponse.respond { content = "Message record removed from ${target.mention}." } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/MuteCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import dev.kord.common.exception.RequestException 4 | import dev.kord.core.behavior.edit 5 | import dev.kord.core.behavior.interaction.response.respond 6 | import kotlinx.datetime.toKotlinInstant 7 | import me.ddivad.judgebot.arguments.LowerMemberArg 8 | import me.ddivad.judgebot.dataclasses.Configuration 9 | import me.ddivad.judgebot.dataclasses.Permissions 10 | import me.ddivad.judgebot.extensions.getHighestRolePosition 11 | import me.ddivad.judgebot.extensions.testDmStatus 12 | import me.ddivad.judgebot.services.infractions.MuteService 13 | import me.ddivad.judgebot.services.infractions.MuteState 14 | import me.jakejmattson.discordkt.arguments.EveryArg 15 | import me.jakejmattson.discordkt.arguments.MemberArg 16 | import me.jakejmattson.discordkt.arguments.TimeArg 17 | import me.jakejmattson.discordkt.commands.commands 18 | import me.jakejmattson.discordkt.extensions.TimeStyle 19 | import me.jakejmattson.discordkt.extensions.TimeStamp 20 | import java.time.Instant 21 | import kotlin.math.roundToLong 22 | 23 | @Suppress("unused") 24 | fun createMuteCommands(muteService: MuteService, configuration: Configuration) = commands("Mute") { 25 | slash("mute", "Mute a user for a specified time.", Permissions.MODERATOR) { 26 | execute(LowerMemberArg("Member", "Target Member"), TimeArg("Time"), EveryArg("Reason")) { 27 | val (targetMember, length, reason) = args 28 | val interactionResponse = interaction?.deferPublicResponse() ?: return@execute 29 | val dmEnabled: Boolean = try { 30 | targetMember.testDmStatus() 31 | true 32 | } catch (ex: RequestException) { 33 | false 34 | } 35 | muteService.applyMuteAndSendReason(targetMember, length.roundToLong() * 1000, reason) 36 | interactionResponse.respond { 37 | content = 38 | "User ${targetMember.mention} has been muted ${if (!dmEnabled) "\n**Note**: user has DMs disabled and won't receive bot messages." else ""}" 39 | } 40 | } 41 | } 42 | 43 | slash("unmute", "Unmute a user.", Permissions.MODERATOR) { 44 | execute(MemberArg) { 45 | val targetMember = args.first 46 | val interactionResponse = interaction?.deferPublicResponse() ?: return@execute 47 | if (muteService.checkMuteState(guild, targetMember) == MuteState.None) { 48 | interactionResponse?.respond { content = "User ${targetMember.mention} isn't muted" } 49 | return@execute 50 | } 51 | muteService.removeMute(guild, targetMember.asUser()) 52 | interactionResponse.respond { 53 | content = "User ${args.first.mention} has been unmuted" 54 | } 55 | } 56 | } 57 | 58 | slash("timeout", "Time a user out", Permissions.MODERATOR) { 59 | execute(LowerMemberArg("Member", "Target Member"), TimeArg) { 60 | val (member, time) = args 61 | val duration = Instant.ofEpochMilli(Instant.now().toEpochMilli() + (time - 2).toLong() * 1000) 62 | member.edit { 63 | communicationDisabledUntil = duration.toKotlinInstant() 64 | } 65 | respond("Member timed out") 66 | } 67 | } 68 | 69 | user("Gag", "gag", "Mute a user for 5 minutes", Permissions.MODERATOR) { 70 | val targetMember = arg.asMemberOrNull(guild.id) ?: return@user 71 | val muteDuration = configuration[guild.id]?.infractionConfiguration?.gagDuration ?: return@user 72 | if (targetMember.getHighestRolePosition() > author.asMember(guild.id).getHighestRolePosition()) { 73 | respond("Missing required permission for target user") 74 | return@user 75 | } 76 | if (muteService.checkMuteState(guild, targetMember) == MuteState.Tracked) { 77 | respond("User ${targetMember.mention} is already muted") 78 | return@user 79 | } 80 | muteService.gag(guild, targetMember, author) 81 | respond( 82 | "${targetMember.mention} has been muted until ${ 83 | TimeStamp.at( 84 | Instant.ofEpochMilli( 85 | Instant.now().toEpochMilli() + muteDuration 86 | ), TimeStyle.TIME_SHORT 87 | ) 88 | }" 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/NoteCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import me.ddivad.judgebot.arguments.LowerUserArg 4 | import me.ddivad.judgebot.dataclasses.Permissions 5 | import me.ddivad.judgebot.services.DatabaseService 6 | import me.jakejmattson.discordkt.arguments.EveryArg 7 | import me.jakejmattson.discordkt.arguments.IntegerArg 8 | import me.jakejmattson.discordkt.commands.subcommand 9 | 10 | @Suppress("unused") 11 | fun noteCommandsSub(databaseService: DatabaseService) = subcommand("Note", Permissions.MODERATOR) { 12 | sub("add", "Use this to add a note to a user.") { 13 | execute(LowerUserArg("User", "Target User"), EveryArg("Content", "Note content")) { 14 | val (target, note) = args 15 | val user = databaseService.users.getOrCreateUser(target, guild) 16 | databaseService.users.addNote(guild, user, note, author.id.toString()) 17 | respondPublic("Note added to ${target.mention}: \n${note}") 18 | } 19 | } 20 | 21 | sub("edit", "Use this to edit a note.") { 22 | execute( 23 | LowerUserArg("User", "Target User"), 24 | IntegerArg("ID", "Note to edit"), 25 | EveryArg("Content", "Note content") 26 | ) { 27 | val (target, noteId, note) = args 28 | val user = databaseService.users.getOrCreateUser(target, guild) 29 | if (user.getGuildInfo(guild.id.toString()).notes.none { it.id == noteId }) { 30 | respond("User has no note with ID $noteId.") 31 | return@execute 32 | } 33 | databaseService.users.editNote(guild, user, noteId, note, author.id.toString()) 34 | respondPublic("Note edited.") 35 | } 36 | } 37 | 38 | sub("delete", "Use this to delete a note from a user.") { 39 | execute(LowerUserArg("User", "Target User"), IntegerArg("ID", "Note ID")) { 40 | val (target, noteId) = args 41 | val user = databaseService.users.getOrCreateUser(target, guild) 42 | if (user.getGuildInfo(guild.id.toString()).notes.isEmpty()) { 43 | respond("User has no notes.") 44 | return@execute 45 | } 46 | databaseService.users.deleteNote(guild, user, noteId) 47 | respondPublic("Note deleted from ${target.mention}.") 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/RuleCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import dev.kord.core.behavior.edit 4 | import dev.kord.rest.builder.message.modify.embed 5 | import me.ddivad.judgebot.arguments.autoCompletingRuleArg 6 | import me.ddivad.judgebot.conversations.rules.AddRuleConversation 7 | import me.ddivad.judgebot.conversations.rules.ArchiveRuleConversation 8 | import me.ddivad.judgebot.conversations.rules.EditRuleConversation 9 | import me.ddivad.judgebot.dataclasses.Permissions 10 | import me.ddivad.judgebot.embeds.createRuleEmbed 11 | import me.ddivad.judgebot.embeds.createRulesEmbed 12 | import me.ddivad.judgebot.embeds.createRulesEmbedDetailed 13 | import me.ddivad.judgebot.services.DatabaseService 14 | import me.jakejmattson.discordkt.arguments.IntegerArg 15 | import me.jakejmattson.discordkt.arguments.MessageArg 16 | import me.jakejmattson.discordkt.commands.commands 17 | import me.jakejmattson.discordkt.commands.subcommand 18 | import me.jakejmattson.discordkt.extensions.jumpLink 19 | 20 | @Suppress("unused") 21 | fun ruleSubCommands(databaseService: DatabaseService) = subcommand("Rule", Permissions.ADMINISTRATOR) { 22 | sub("add", "Add a rule to this guild.") { 23 | execute { 24 | AddRuleConversation(databaseService) 25 | .createAddRuleConversation(guild) 26 | .startSlashResponse(discord, author, this) 27 | } 28 | } 29 | sub("edit", "Edit a rule in this guild.") { 30 | execute(autoCompletingRuleArg(databaseService)) { 31 | EditRuleConversation(databaseService) 32 | .createAddRuleConversation(guild, args.first) 33 | .startSlashResponse(discord, author, this) 34 | } 35 | } 36 | sub("archive", "Archive a rule in this guild.") { 37 | execute { 38 | ArchiveRuleConversation(databaseService) 39 | .createArchiveRuleConversation(guild) 40 | .startSlashResponse(discord, author, this) 41 | } 42 | } 43 | } 44 | 45 | @Suppress("unused") 46 | fun ruleCommands(databaseService: DatabaseService) = commands("Rules") { 47 | slash( 48 | "rules", 49 | "List the rules of this guild. Pass a message ID to edit existing rules embed.", 50 | Permissions.EVERYONE 51 | ) { 52 | execute(MessageArg.optionalNullable(null)) { 53 | val messageToEdit = args.first 54 | if (messageToEdit != null) { 55 | messageToEdit.edit { 56 | this.embeds?.first()?.createRulesEmbed(guild, databaseService.guilds.getRules(guild)) 57 | } 58 | respond("Existing embed updated: ${messageToEdit.jumpLink()}") 59 | } else { 60 | respondPublic { 61 | createRulesEmbed(guild, databaseService.guilds.getRules(guild)) 62 | } 63 | } 64 | } 65 | } 66 | 67 | slash( 68 | "longRules", 69 | "List the rules (with descriptions) of this guild. Pass a message ID to edit existing rules embed.", 70 | Permissions.STAFF 71 | ) { 72 | execute(MessageArg.optionalNullable(null)) { 73 | val messageToEdit = args.first 74 | if (messageToEdit != null) { 75 | messageToEdit.edit { 76 | embed { createRulesEmbedDetailed(guild, databaseService.guilds.getRules(guild)) } 77 | } 78 | respondPublic("Existing embed updated: ${messageToEdit.jumpLink()}") 79 | } else { 80 | respondPublic { 81 | createRulesEmbedDetailed(guild, databaseService.guilds.getRules(guild)) 82 | } 83 | } 84 | } 85 | } 86 | 87 | slash("viewRule", "List a rule from this guild.", Permissions.EVERYONE) { 88 | execute(autoCompletingRuleArg(databaseService)) { 89 | val rule = databaseService.guilds.getRule(guild, args.first.split(" -").first().toInt())!! 90 | respondPublic { 91 | createRuleEmbed(guild, rule) 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/UserCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import dev.kord.common.entity.ApplicationCommandType 4 | import dev.kord.common.kColor 5 | import dev.kord.core.behavior.interaction.response.respond 6 | import dev.kord.rest.Image 7 | import dev.kord.x.emoji.Emojis 8 | import me.ddivad.judgebot.arguments.LowerUserArg 9 | import me.ddivad.judgebot.dataclasses.Ban 10 | import me.ddivad.judgebot.dataclasses.Configuration 11 | import me.ddivad.judgebot.dataclasses.Permissions 12 | import me.ddivad.judgebot.embeds.createHistoryEmbed 13 | import me.ddivad.judgebot.embeds.createLinkedAccountMenu 14 | import me.ddivad.judgebot.services.DatabaseService 15 | import me.ddivad.judgebot.services.LoggingService 16 | import me.ddivad.judgebot.services.infractions.BanService 17 | import me.jakejmattson.discordkt.arguments.* 18 | import me.jakejmattson.discordkt.commands.commands 19 | import me.jakejmattson.discordkt.commands.subcommand 20 | import me.jakejmattson.discordkt.extensions.createMenu 21 | import me.jakejmattson.discordkt.extensions.pfpUrl 22 | import java.awt.Color 23 | import java.text.SimpleDateFormat 24 | 25 | @Suppress("unused") 26 | fun createUserCommands( 27 | databaseService: DatabaseService, 28 | config: Configuration, 29 | loggingService: LoggingService, 30 | banService: BanService 31 | ) = commands("User") { 32 | slash("history", "Use this to view a user's record.", Permissions.MODERATOR) { 33 | execute(UserArg) { 34 | val interactionResponse = interaction?.deferPublicResponse() ?: return@execute 35 | val user = databaseService.users.getOrCreateUser(args.first, guild) 36 | databaseService.users.incrementUserHistory(user, guild) 37 | channel.createMenu { createHistoryEmbed(args.first, user, guild, config, databaseService) } 38 | interactionResponse.respond { content = "History for ${args.first.mention}:" } 39 | } 40 | } 41 | 42 | slash("ban", "Ban a member from this guild.", Permissions.STAFF) { 43 | execute(LowerUserArg, EveryArg("Reason"), IntegerArg("Days", "Delete message days").optional(0)) { 44 | val (target, reason, deleteDays) = args 45 | if (deleteDays > 7) { 46 | respond("Delete days cannot be more than **7**. You tried with **${deleteDays}**") 47 | return@execute 48 | } 49 | val ban = Ban(target.id.toString(), author.id.toString(), reason) 50 | banService.banUser(target, guild, ban, deleteDays).also { 51 | loggingService.userBanned(guild, target, ban) 52 | respondPublic("User ${target.mention} banned\n**Reason**: $reason") 53 | } 54 | } 55 | } 56 | 57 | slash("unban", "Unban a banned member from this guild.", Permissions.STAFF) { 58 | execute( 59 | UserArg, 60 | BooleanArg( 61 | "Thin-Ice", 62 | "true", 63 | "false", 64 | "Unban user in 'Thin Ice' mode which sets their points to 40 and freezes their point decay" 65 | ).optional(false) 66 | ) { 67 | val (user, thinIce) = args 68 | guild.getBanOrNull(user.id)?.let { 69 | banService.unbanUser(user, guild, thinIce) 70 | respondPublic("${user.tag} unbanned ${if (thinIce) "in thin ice mode" else ""}") 71 | return@execute 72 | } 73 | respondPublic("${user.mention} isn't banned from this guild.") 74 | } 75 | } 76 | 77 | slash("alt", "Link, Unlink or view a user's alt accounts", Permissions.STAFF) { 78 | execute( 79 | ChoiceArg("Option", "Alt options", "Link", "Unlink", "View"), 80 | UserArg("Main"), 81 | UserArg("Alt").optionalNullable(null) 82 | ) { 83 | val (option, user, alt) = args 84 | val mainRecord = databaseService.users.getOrCreateUser(user, guild) 85 | when (option) { 86 | "Link" -> { 87 | if (alt == null) { 88 | respond("Missing alt account argument") 89 | return@execute 90 | } 91 | val altRecord = databaseService.users.getOrCreateUser(alt, guild) 92 | databaseService.users.addLinkedAccount(guild, mainRecord, alt.id.toString()) 93 | databaseService.users.addLinkedAccount(guild, altRecord, user.id.toString()) 94 | respondPublic("Linked accounts ${user.mention} and ${alt.mention}") 95 | } 96 | "Unlink" -> { 97 | if (alt == null) { 98 | respond("Missing alt account argument") 99 | return@execute 100 | } 101 | val altRecord = databaseService.users.getOrCreateUser(alt, guild) 102 | databaseService.users.removeLinkedAccount(guild, mainRecord, alt.id.toString()) 103 | databaseService.users.removeLinkedAccount(guild, altRecord, user.id.toString()) 104 | respondPublic("Unlinked accounts ${user.mention} and ${alt.mention}") 105 | } 106 | "View" -> { 107 | databaseService.users.incrementUserHistory(mainRecord, guild) 108 | val linkedAccounts = mainRecord.getLinkedAccounts(guild) 109 | 110 | if (linkedAccounts.isEmpty()) { 111 | respond("User ${user.mention} has no alt accounts recorded.") 112 | return@execute 113 | } 114 | respondPublic("Alt accounts for ${user.mention}") 115 | channel.createMenu { createLinkedAccountMenu(linkedAccounts, guild, config, databaseService) } 116 | } 117 | } 118 | } 119 | } 120 | 121 | user("PFP Lookup", "whatpfp","Perform a reverse image search of a User's profile picture", Permissions.EVERYONE) { 122 | val user = args.first 123 | val reverseSearchUrl = "" 124 | 125 | if (interaction?.invokedCommandType == ApplicationCommandType.User) { 126 | respond { 127 | title = "${user.tag}'s pfp" 128 | color = Color.MAGENTA.kColor 129 | description = "[Reverse Search]($reverseSearchUrl)" 130 | image = "${user.pfpUrl}?size=512" 131 | } 132 | } else { 133 | respondPublic { 134 | title = "${user.tag}'s pfp" 135 | color = Color.MAGENTA.kColor 136 | description = "[Reverse Search]($reverseSearchUrl)" 137 | image = "${user.pfpUrl}?size=512" 138 | } 139 | } 140 | 141 | } 142 | 143 | slash("deletedMessages", "View a users messages deleted using the delete message reaction", Permissions.STAFF) { 144 | execute(UserArg) { 145 | val target = args.first 146 | val guildMember = databaseService.users.getOrCreateUser(target, guild).getGuildInfo(guild.id.toString()) 147 | val guildConfiguration = config[guild.asGuild().id] 148 | 149 | val deletedMessages = databaseService.messageDeletes 150 | .getMessageDeletesForMember(guild.id.toString(), target.id.toString()) 151 | .sortedByDescending { it.dateTime } 152 | .map { "Deleted on **${SimpleDateFormat("dd/MM/yyyy HH:mm").format(it.dateTime)}** \n[Message Link](${it.messageLink})" } 153 | .chunked(6) 154 | 155 | if (deletedMessages.isEmpty()) { 156 | respond("User has no messages deleted using ${guildConfiguration?.reactions?.deleteMessageReaction}") 157 | return@execute 158 | } 159 | 160 | respondPublic("Deleted messages for ${target.mention}") 161 | channel.createMenu { 162 | deletedMessages.forEachIndexed { index, list -> 163 | page { 164 | color = discord.configuration.theme 165 | author { 166 | name = "Deleted messages for ${target.tag}" 167 | icon = target.pfpUrl 168 | } 169 | description = """ 170 | **Showing messages deleted using ${guildConfiguration?.reactions?.deleteMessageReaction}** 171 | ${target.tag} has **${guildMember.deletedMessageCount.deleteReaction}** deletions 172 | """.trimIndent() 173 | 174 | list.forEach { 175 | field { 176 | value = it 177 | } 178 | } 179 | 180 | footer { 181 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 182 | text = "${guild.name} | Page ${index + 1} of ${deletedMessages.size}" 183 | } 184 | } 185 | } 186 | if (deletedMessages.size > 1) { 187 | buttons { 188 | button("Prev.", Emojis.arrowLeft) { 189 | previousPage() 190 | } 191 | button("Next", Emojis.arrowRight) { 192 | nextPage() 193 | } 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | @Suppress("Unused") 202 | fun banReasonSubCommands(databaseService: DatabaseService) = subcommand("BanReason", Permissions.STAFF) { 203 | sub("get", "Get the ban reason for a user if it exists.") { 204 | execute(UserArg) { 205 | val user = args.first 206 | guild.getBanOrNull(user.id)?.let { 207 | val reason = databaseService.guilds.getBanOrNull(guild, user.id.toString())?.reason ?: it.reason 208 | respondPublic(reason ?: "No reason logged") 209 | return@execute 210 | } 211 | respond("${user.username} isn't banned from this guild.") 212 | } 213 | } 214 | 215 | sub("set", "Set a ban reason for a user.") { 216 | execute(UserArg, EveryArg("Reason")) { 217 | val (user, reason) = args 218 | val ban = Ban(user.id.toString(), author.id.toString(), reason) 219 | if (guild.getBanOrNull(user.id) != null) { 220 | if (!databaseService.guilds.checkBanExists(guild, user.id.toString())) { 221 | databaseService.guilds.addBan(guild, ban) 222 | } else { 223 | databaseService.guilds.editBanReason(guild, user.id.toString(), reason) 224 | } 225 | respondPublic("Ban reason for ${user.username} set to: $reason") 226 | } else respond("User ${user.username} isn't banned") 227 | } 228 | } 229 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/commands/UtilityCommands.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.commands 2 | 3 | import me.ddivad.judgebot.dataclasses.Configuration 4 | import me.ddivad.judgebot.dataclasses.Permissions 5 | import me.ddivad.judgebot.embeds.createSelfHistoryEmbed 6 | import me.ddivad.judgebot.services.DatabaseService 7 | import me.jakejmattson.discordkt.commands.commands 8 | 9 | @Suppress("unused") 10 | fun createInformationCommands( 11 | configuration: Configuration, 12 | databaseService: DatabaseService, 13 | ) = commands("Utility") { 14 | slash("selfHistory", "View your infraction history.", Permissions.EVERYONE) { 15 | execute { 16 | val user = author 17 | val guildMember = databaseService.users.getOrCreateUser(user, guild) 18 | respond { createSelfHistoryEmbed(user, guildMember, guild, configuration) } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/conversations/rules/AddRuleConversation.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.conversations.rules 2 | 3 | import dev.kord.core.entity.Guild 4 | import me.ddivad.judgebot.dataclasses.Rule 5 | import me.ddivad.judgebot.embeds.createRuleEmbed 6 | import me.ddivad.judgebot.services.DatabaseService 7 | import me.jakejmattson.discordkt.arguments.BooleanArg 8 | import me.jakejmattson.discordkt.arguments.EveryArg 9 | import me.jakejmattson.discordkt.arguments.UrlArg 10 | import me.jakejmattson.discordkt.conversations.slashConversation 11 | 12 | class AddRuleConversation(private val databaseService: DatabaseService) { 13 | fun createAddRuleConversation(guild: Guild) = slashConversation("cancel") { 14 | val rules = databaseService.guilds.getRules(guild) 15 | val nextId = rules.size.plus(1) 16 | 17 | val ruleName = prompt(EveryArg, "Please enter rule name:") 18 | val ruleText = prompt(EveryArg, "Please enter rule text") 19 | val addLink = prompt( 20 | BooleanArg("Add link to rule?", "Y", "N"), 21 | "Do you want to add a link to the rule? (Y/N)" 22 | ) 23 | val ruleLink = when { 24 | addLink -> prompt(UrlArg, "Please enter the link") 25 | else -> "" 26 | } 27 | 28 | val newRule = Rule(nextId, ruleName, ruleText, ruleLink) 29 | databaseService.guilds.addRule(guild, newRule) 30 | respond("Rule created.") 31 | respond { 32 | createRuleEmbed(guild, newRule) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/conversations/rules/ArchiveRuleConversation.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.conversations.rules 2 | 3 | import dev.kord.core.entity.Guild 4 | import me.ddivad.judgebot.services.DatabaseService 5 | import me.jakejmattson.discordkt.arguments.IntegerArg 6 | import me.jakejmattson.discordkt.conversations.slashConversation 7 | 8 | class ArchiveRuleConversation(private val databaseService: DatabaseService) { 9 | fun createArchiveRuleConversation(guild: Guild) = slashConversation("cancel") { 10 | val ruleToArchive = prompt(IntegerArg, "Please enter rule number to archive:") 11 | 12 | databaseService.guilds.archiveRule(guild, ruleToArchive) 13 | respond("Rule archived.") 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/conversations/rules/EditRuleConversation.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.conversations.rules 2 | 3 | import dev.kord.core.entity.Guild 4 | import me.ddivad.judgebot.dataclasses.Rule 5 | import me.ddivad.judgebot.embeds.createRuleEmbed 6 | import me.ddivad.judgebot.services.DatabaseService 7 | import me.jakejmattson.discordkt.arguments.BooleanArg 8 | import me.jakejmattson.discordkt.arguments.EveryArg 9 | import me.jakejmattson.discordkt.arguments.IntegerArg 10 | import me.jakejmattson.discordkt.arguments.UrlArg 11 | import me.jakejmattson.discordkt.conversations.slashConversation 12 | 13 | class EditRuleConversation(private val databaseService: DatabaseService) { 14 | fun createAddRuleConversation(guild: Guild, ruleName: String) = slashConversation("cancel") { 15 | val rules = databaseService.guilds.getRulesForInfractionPrompt(guild) 16 | val ruleToUpdate = rules.find { it.number == ruleName.split(" -").first().toInt() } ?: return@slashConversation 17 | respond("Current Rule:") 18 | respond { 19 | createRuleEmbed(guild, ruleToUpdate) 20 | } 21 | 22 | val updateNumber = prompt( 23 | BooleanArg(truthValue = "y", falseValue = "n"), 24 | "Update Rule number? (Y/N)" 25 | ) 26 | val ruleNumber = when { 27 | updateNumber -> promptUntil( 28 | argument = IntegerArg, 29 | prompt = "Please enter rule number:", 30 | isValid = { number -> !rules.any { it.number == number } }, 31 | error = "Rule with that number already exists" 32 | ) 33 | else -> ruleToUpdate.number 34 | } 35 | val updateName = prompt( 36 | BooleanArg(truthValue = "y", falseValue = "n"), 37 | "Update Rule name? (Y/N)" 38 | ) 39 | val ruleName = when { 40 | updateName -> promptUntil( 41 | EveryArg, 42 | "Please enter rule name:", 43 | "Rule with that name already exists", 44 | isValid = { name -> !rules.any { it.title == name } } 45 | ) 46 | else -> ruleToUpdate.title 47 | } 48 | 49 | val updateText = prompt( 50 | BooleanArg(truthValue = "y", falseValue = "n"), 51 | "Update Rule text? (Y/N)" 52 | ) 53 | val ruleText = when { 54 | updateText -> prompt(EveryArg, "Please enter rule text:") 55 | else -> ruleToUpdate.description 56 | } 57 | 58 | val updateLink = prompt( 59 | BooleanArg(truthValue = "y", falseValue = "n"), 60 | "Update Rule link? (Y/N)" 61 | ) 62 | val ruleLink = when { 63 | updateLink -> prompt(UrlArg, "Please enter the link") 64 | else -> ruleToUpdate.link 65 | } 66 | 67 | val newRule = Rule(ruleNumber, ruleName, ruleText, ruleLink, ruleToUpdate.archived) 68 | databaseService.guilds.editRule(guild, ruleToUpdate, newRule) 69 | respond("Rule edited.") 70 | respond { 71 | createRuleEmbed(guild, newRule) 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/dataclasses/Configuration.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.dataclasses 2 | 3 | import dev.kord.common.entity.Snowflake 4 | import dev.kord.core.entity.Guild 5 | import dev.kord.x.emoji.Emojis 6 | import kotlinx.serialization.Serializable 7 | import me.jakejmattson.discordkt.dsl.Data 8 | import me.jakejmattson.discordkt.dsl.edit 9 | import kotlin.time.DurationUnit 10 | import kotlin.time.toDuration 11 | 12 | @Serializable 13 | data class Configuration( 14 | val guildConfigurations: MutableMap = mutableMapOf(), 15 | val dbConfiguration: DatabaseConfiguration = DatabaseConfiguration() 16 | ) : Data() { 17 | operator fun get(id: Snowflake) = guildConfigurations[id] 18 | fun setup( 19 | guild: Guild, configuration: GuildConfiguration 20 | ) { 21 | if (guildConfigurations[guild.id] != null) return 22 | 23 | // Setup default punishments 24 | // TODO: Add configuration commands for this 25 | configuration.punishments.add(PunishmentLevel(0, PunishmentType.NONE, 0L)) 26 | configuration.punishments.add( 27 | PunishmentLevel( 28 | 10, PunishmentType.MUTE, 1.toDuration(DurationUnit.HOURS).inWholeMilliseconds 29 | ) 30 | ) 31 | configuration.punishments.add( 32 | PunishmentLevel( 33 | 20, PunishmentType.MUTE, 12.toDuration(DurationUnit.HOURS).inWholeMilliseconds 34 | ) 35 | ) 36 | configuration.punishments.add( 37 | PunishmentLevel( 38 | 30, PunishmentType.MUTE, 24.toDuration(DurationUnit.HOURS).inWholeMilliseconds 39 | ) 40 | ) 41 | configuration.punishments.add( 42 | PunishmentLevel( 43 | 40, PunishmentType.MUTE, 28.toDuration(DurationUnit.DAYS).inWholeMilliseconds 44 | ) 45 | ) 46 | configuration.punishments.add(PunishmentLevel(50, PunishmentType.BAN)) 47 | 48 | edit { guildConfigurations[guild.id] = configuration } 49 | } 50 | } 51 | 52 | @Serializable 53 | data class DatabaseConfiguration( 54 | val address: String = "mongodb://localhost:27017", val databaseName: String = "judgebot" 55 | ) 56 | 57 | @Serializable 58 | data class GuildConfiguration( 59 | var prefix: String = "j!", 60 | var moderatorRoles: MutableList = mutableListOf(), 61 | var staffRoles: MutableList = mutableListOf(), 62 | var adminRoles: MutableList = mutableListOf(), 63 | var mutedRole: Snowflake, 64 | var loggingConfiguration: LoggingConfiguration, 65 | var infractionConfiguration: InfractionConfiguration = InfractionConfiguration(), 66 | var punishments: MutableList = mutableListOf(), 67 | var reactions: ReactionConfiguration = ReactionConfiguration() 68 | ) 69 | 70 | @Serializable 71 | data class LoggingConfiguration( 72 | var alertChannel: Snowflake, 73 | var loggingChannel: Snowflake, 74 | ) 75 | 76 | @Serializable 77 | data class InfractionConfiguration( 78 | var pointCeiling: Int = 50, 79 | var strikePoints: Int = 10, 80 | var warnPoints: Int = 0, 81 | var warnUpgradeThreshold: Int = 40, 82 | var pointDecayPerWeek: Int = 2, 83 | var gagDuration: Long = 5.toDuration(DurationUnit.MINUTES).inWholeMilliseconds 84 | ) 85 | 86 | @Serializable 87 | data class PunishmentLevel( 88 | var points: Int = 0, var punishment: PunishmentType, var duration: Long? = null 89 | ) 90 | 91 | @Serializable 92 | data class ReactionConfiguration( 93 | var enabled: Boolean = true, 94 | var gagReaction: String = "${Emojis.mute}", 95 | var deleteMessageReaction: String = "${Emojis.wastebasket}", 96 | var flagMessageReaction: String = "${Emojis.stopSign}" 97 | ) -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/dataclasses/GuildInformation.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.dataclasses 2 | 3 | data class Rule( 4 | val number: Int, 5 | val title: String, 6 | val description: String, 7 | val link: String, 8 | var archived: Boolean = false 9 | ) 10 | 11 | data class GuildInformation( 12 | val guildId: String, 13 | val guildName: String, 14 | val rules: MutableList = mutableListOf(), 15 | val bans: MutableList = mutableListOf(), 16 | val punishments: MutableList = mutableListOf() 17 | ) { 18 | fun addRule(rule: Rule): GuildInformation = this.apply { 19 | this.rules.add(rule) 20 | } 21 | 22 | fun archiveRule(ruleNumber: Int): GuildInformation = this.apply { 23 | this.rules.find { it.number == ruleNumber }?.archived = true 24 | } 25 | 26 | fun editRule(oldRule: Rule, updatedRule: Rule): Rule { 27 | val index = this.rules.indexOf(oldRule) 28 | this.rules[index] = updatedRule 29 | return updatedRule 30 | } 31 | 32 | fun addPunishment(punishment: Punishment): GuildInformation = this.apply { 33 | val nextId: Int = if (this.punishments.isEmpty()) 1 else this.punishments.maxByOrNull { it.id }!!.id + 1 34 | punishment.id = nextId 35 | this.punishments.add(punishment) 36 | } 37 | 38 | fun removePunishment(userId: String, type: InfractionType): GuildInformation = this.apply { 39 | val punishment = this.getPunishmentByType(type, userId).firstOrNull() ?: return@apply 40 | this.punishments.remove(punishment) 41 | } 42 | 43 | fun getPunishmentByType(type: InfractionType, userId: String): List { 44 | return this.punishments.filter { it.type == type && it.userId == userId } 45 | } 46 | 47 | fun getPunishmentsByUser(userId: String): List { 48 | return this.punishments.filter { it.userId == userId } 49 | } 50 | 51 | fun checkBanExits(userId: String): Boolean { 52 | return this.bans.any { it.userId == userId } 53 | } 54 | 55 | fun addBan(ban: Ban): GuildInformation = this.apply { 56 | this.bans.add(ban) 57 | } 58 | 59 | fun removeBan(userId: String): GuildInformation = this.apply { 60 | val ban = this.bans.find { it.userId == userId } 61 | this.bans.remove(ban) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/dataclasses/GuildMember.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.dataclasses 2 | 3 | import dev.kord.core.entity.Guild 4 | import me.ddivad.judgebot.services.LoggingService 5 | import me.jakejmattson.discordkt.extensions.TimeStamp 6 | import mu.KotlinLogging 7 | import java.time.Instant 8 | import java.time.temporal.ChronoUnit 9 | import kotlin.time.DurationUnit 10 | import kotlin.time.toDuration 11 | val logger = KotlinLogging.logger { } 12 | 13 | data class GuildMemberDetails( 14 | val guildId: String, 15 | val notes: MutableList = mutableListOf(), 16 | val infractions: MutableList = mutableListOf(), 17 | val info: MutableList = mutableListOf(), 18 | val linkedAccounts: MutableList = mutableListOf(), 19 | val bans: MutableList = mutableListOf(), 20 | var historyCount: Int = 0, 21 | var points: Int = 0, 22 | var pointDecayTimer: Long = Instant.now().toEpochMilli(), 23 | var lastInfraction: Long = 0, 24 | val deletedMessageCount: DeletedMessages = DeletedMessages(), 25 | var pointDecayFrozen: Boolean = false 26 | ) 27 | 28 | data class DeletedMessages( 29 | var deleteReaction: Int = 0, 30 | var total: Int = 0 31 | ) 32 | 33 | data class GuildMember( 34 | val userId: String, 35 | val guilds: MutableList = mutableListOf() 36 | ) { 37 | 38 | fun addNote(note: String, moderator: String, guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 39 | val nextId: Int = if (this.notes.isEmpty()) 1 else this.notes.maxByOrNull { it.id }!!.id + 1 40 | this.notes.add(Note(note, moderator,Instant.now().toEpochMilli(), nextId)) 41 | } 42 | 43 | fun editNote(guild: Guild, noteId: Int, newNote: String, moderator: String) = 44 | with(this.getGuildInfo(guild.id.toString())) { 45 | this.notes.find { it.id == noteId }?.let { 46 | it.note = newNote 47 | it.moderator = moderator 48 | } 49 | } 50 | 51 | fun deleteNote(noteId: Int, guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 52 | this.notes.removeIf { it.id == noteId } 53 | } 54 | 55 | fun addInfo(information: Info, guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 56 | val nextId: Int = if (this.info.isEmpty()) 1 else this.info.maxByOrNull { it.id!! }!!.id!! + 1 57 | information.id = nextId 58 | this.info.add(information) 59 | } 60 | 61 | fun removeInfo(id: Int, guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 62 | this.info.removeIf { it.id == id } 63 | } 64 | 65 | fun addLinkedAccount(guild: Guild, userId: String) = with(this.getGuildInfo(guild.id.toString())) { 66 | this.linkedAccounts.find { it == userId }.let { 67 | if (it == null) { 68 | this.linkedAccounts.add(userId) 69 | } 70 | return@let 71 | } 72 | } 73 | 74 | fun getLinkedAccounts(guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 75 | this.linkedAccounts 76 | } 77 | 78 | fun removeLinkedAccount(guild: Guild, userId: String) = with(this.getGuildInfo(guild.id.toString())) { 79 | this.linkedAccounts.removeIf { it == userId } 80 | } 81 | 82 | fun cleanseNotes(guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 83 | this.notes.clear() 84 | } 85 | 86 | private fun cleanseInfo(guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 87 | this.info.clear() 88 | } 89 | 90 | fun cleanseInfractions(guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 91 | this.infractions.clear() 92 | this.points = 0 93 | } 94 | 95 | fun deleteInfraction(guild: Guild, infractionId: Int) = with(this.getGuildInfo(guild.id.toString())) { 96 | this.infractions.find { it.id == infractionId }?.let { 97 | this.infractions.remove(it) 98 | this.points -= it.points 99 | } 100 | } 101 | 102 | fun addInfraction(infraction: Infraction, guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 103 | val nextId: Int = if (this.infractions.isEmpty()) 1 else this.infractions.maxByOrNull { it.id!! }?.id!! + 1 104 | infraction.id = nextId 105 | this.infractions.add(infraction) 106 | this.points += infraction.points 107 | this.lastInfraction = Instant.now().toEpochMilli() 108 | } 109 | 110 | fun incrementHistoryCount(guildId: String) { 111 | this.getGuildInfo(guildId).historyCount += 1 112 | } 113 | 114 | fun updatePointDecayDate(guild: Guild, punishmentDuration: Long) = with(this.getGuildInfo(guild.id.toString())) { 115 | this.pointDecayTimer = Instant.now().toEpochMilli().plus(punishmentDuration) 116 | } 117 | 118 | fun addMessageDeleted(guild: Guild, deleteReaction: Boolean) = with(this.getGuildInfo(guild.id.toString())) { 119 | this.deletedMessageCount.total++ 120 | if (deleteReaction) this.deletedMessageCount.deleteReaction++ 121 | } 122 | 123 | suspend fun checkPointDecay(guild: Guild, configuration: GuildConfiguration, loggingService: LoggingService) = 124 | with(this.getGuildInfo(guild.id.toString())) { 125 | if (bans.lastOrNull()?.thinIce == true && this.pointDecayFrozen && Instant.now() 126 | .toEpochMilli() >= this.pointDecayTimer 127 | ) { 128 | this.pointDecayTimer = Instant.now().toEpochMilli() 129 | this.pointDecayFrozen = false 130 | } else if (this.pointDecayFrozen) { 131 | return@with 132 | } 133 | val weeksSincePointsDecayed = (ChronoUnit.DAYS.between(Instant.ofEpochMilli(this.pointDecayTimer), Instant.now()) / 7).toInt() 134 | logger.debug { "Point decay: $weeksSincePointsDecayed - $points - $pointDecayTimer" } 135 | if (weeksSincePointsDecayed > 0) { 136 | if (this.points > 0) { 137 | val pointsToRemove = 138 | configuration.infractionConfiguration.pointDecayPerWeek * weeksSincePointsDecayed 139 | if (pointsToRemove > this.points) { 140 | this.points = 0 141 | } else this.points -= pointsToRemove 142 | loggingService.pointDecayApplied( 143 | guild, 144 | this@GuildMember, 145 | this.points, 146 | pointsToRemove, 147 | weeksSincePointsDecayed 148 | ) 149 | } 150 | this.pointDecayTimer = Instant.now().toEpochMilli() 151 | logger.debug { "Point decay timer set to $pointDecayTimer" } 152 | } 153 | } 154 | 155 | fun updatePointDecayState(guild: Guild, frozen: Boolean) = with(this.getGuildInfo(guild.id.toString())) { 156 | this.pointDecayFrozen = frozen 157 | if (frozen) { 158 | addNote("Point decay frozen on ${TimeStamp.now()}", "${guild.kord.selfId}", guild) 159 | } else { 160 | this.pointDecayTimer = Instant.now().toEpochMilli() 161 | } 162 | } 163 | 164 | fun getPoints(guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 165 | return@with this.points 166 | } 167 | 168 | fun getTotalHistoricalPoints(guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 169 | return@with infractions.sumOf { it.points } 170 | } 171 | 172 | fun enableThinIce(guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 173 | this.points = 40 174 | this.pointDecayTimer = Instant.now().toEpochMilli().plus(60.toDuration(DurationUnit.DAYS).inWholeMilliseconds) 175 | this.pointDecayFrozen = true 176 | } 177 | 178 | fun addBan(guild: Guild, ban: Ban) = with(this.getGuildInfo(guild.id.toString())) { 179 | this.bans.add(ban) 180 | } 181 | 182 | fun unban(guild: Guild, thinIce: Boolean, thinIcePoints: Int) = with(this.getGuildInfo(guild.id.toString())) { 183 | this.bans.lastOrNull().let { 184 | it?.unbanTime = Instant.now().toEpochMilli() 185 | it?.thinIce = thinIce 186 | } 187 | if (thinIce) { 188 | enableThinIce(guild) 189 | } 190 | addNote( 191 | "Unbanned on ${TimeStamp.now()} ${ 192 | if (thinIce) "with Thin Ice enabled. Points were set to $thinIcePoints and frozen until ${ 193 | TimeStamp.at( 194 | Instant.ofEpochMilli(this.pointDecayTimer) 195 | ) 196 | }" else "" 197 | }", "${guild.kord.selfId}", guild 198 | ) 199 | } 200 | 201 | fun reset(guild: Guild) = with(this.getGuildInfo(guild.id.toString())) { 202 | this.points = 0 203 | this.historyCount = 0 204 | this.deletedMessageCount.deleteReaction = 0 205 | cleanseInfo(guild) 206 | cleanseInfractions(guild) 207 | cleanseNotes(guild) 208 | } 209 | 210 | fun ensureGuildDetailsPresent(guildId: String) { 211 | if (this.guilds.any { it.guildId == guildId }) return 212 | this.guilds.add(GuildMemberDetails(guildId)) 213 | } 214 | 215 | fun getGuildInfo(guildId: String): GuildMemberDetails { 216 | return this.guilds.firstOrNull { it.guildId == guildId } ?: GuildMemberDetails(guildId) 217 | } 218 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/dataclasses/Infraction.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.dataclasses 2 | 3 | import java.time.Instant 4 | 5 | enum class InfractionType { 6 | Warn, Strike, Mute, BadPfp, Ban 7 | } 8 | 9 | data class Infraction( 10 | val moderator: String, 11 | val reason: String, 12 | val type: InfractionType, 13 | var points: Int = 0, 14 | val ruleNumber: Int? = null, 15 | val dateTime: Long = Instant.now().toEpochMilli(), 16 | var punishment: PunishmentLevel? = null, 17 | var id: Int? = null 18 | ) 19 | 20 | data class Punishment( 21 | val userId: String, 22 | val type: InfractionType, 23 | val clearTime: Long? = null, 24 | var id: Int = 0 25 | ) 26 | 27 | data class Ban( 28 | val userId: String, 29 | val moderator: String, 30 | var reason: String, 31 | var dateTime: Long? = Instant.now().toEpochMilli(), 32 | var unbanTime: Long? = null, 33 | var thinIce: Boolean = false 34 | ) 35 | 36 | enum class PunishmentType { 37 | MUTE, BAN, NONE 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/dataclasses/JoinLeave.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.dataclasses 2 | 3 | data class JoinLeave( 4 | val guildId: String, 5 | val userId: String, 6 | val joinDate: Long, 7 | var leaveDate: Long? = null 8 | ) -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/dataclasses/MessageDelete.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.dataclasses 2 | 3 | import java.time.Instant 4 | 5 | data class MessageDelete( 6 | val userId: String, 7 | val guildId: String, 8 | val messageLink: String?, 9 | val dateTime: Long = Instant.now().toEpochMilli(), 10 | ) -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/dataclasses/Meta.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.dataclasses 2 | 3 | data class Meta( 4 | val version: Int, 5 | val _id: String = "meta" 6 | ) -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/dataclasses/Note.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.dataclasses 2 | 3 | import java.time.Instant 4 | 5 | data class Note( 6 | var note: String, 7 | var moderator: String, 8 | val dateTime: Long, 9 | val id: Int 10 | ) 11 | 12 | data class Info( 13 | val message: String, 14 | val moderator: String, 15 | val dateTime: Long = Instant.now().toEpochMilli(), 16 | var id: Int? = null 17 | ) -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/dataclasses/Permissions.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.dataclasses 2 | 3 | import dev.kord.common.entity.Permission 4 | import dev.kord.common.entity.Permissions 5 | 6 | @Suppress("unused") 7 | object Permissions { 8 | val GUILD_OWNER = Permissions(Permission.ManageGuild) 9 | val ADMINISTRATOR = Permissions(Permission.ManageGuild) 10 | val STAFF = Permissions(Permission.BanMembers) 11 | val MODERATOR = Permissions(Permission.ManageMessages) 12 | val EVERYONE = Permissions(Permission.UseApplicationCommands) 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/embeds/GuildEmbeds.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.embeds 2 | 3 | import dev.kord.common.entity.Snowflake 4 | import dev.kord.common.kColor 5 | import dev.kord.core.entity.Guild 6 | import dev.kord.rest.Image 7 | import dev.kord.rest.builder.message.EmbedBuilder 8 | import me.ddivad.judgebot.dataclasses.GuildConfiguration 9 | import me.ddivad.judgebot.dataclasses.Punishment 10 | import me.ddivad.judgebot.util.timeToString 11 | import me.jakejmattson.discordkt.extensions.TimeStamp 12 | import me.jakejmattson.discordkt.extensions.TimeStyle 13 | import me.jakejmattson.discordkt.extensions.addField 14 | import java.awt.Color 15 | import java.time.Instant 16 | 17 | suspend fun EmbedBuilder.createConfigEmbed(config: GuildConfiguration, guild: Guild) { 18 | title = "Configuration" 19 | color = Color.MAGENTA.kColor 20 | thumbnail { 21 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 22 | } 23 | 24 | field { 25 | name = "**General:**" 26 | value = "Bot Prefix: ${config.prefix} \n" + 27 | "Admin Roles: ${config.adminRoles.map { guild.getRoleOrNull(it)?.mention }} \n" + 28 | "Staff Roles: ${config.staffRoles.map { guild.getRoleOrNull(it)?.mention }} \n" + 29 | "Moderator Roles: ${config.moderatorRoles.map { guild.getRoleOrNull(it)?.mention }} \n" + 30 | "Mute Role: ${guild.getRoleOrNull(config.mutedRole)?.mention} \n" 31 | } 32 | 33 | field { 34 | name = "**Infractions:**" 35 | value = "Point Ceiling: ${config.infractionConfiguration.pointCeiling} \n" + 36 | "Strike points: ${config.infractionConfiguration.strikePoints} \n" + 37 | "Warn Points: ${config.infractionConfiguration.warnPoints} \n" + 38 | "Warn Upgrade Prompt: ${config.infractionConfiguration.warnUpgradeThreshold} Points\n" + 39 | "Point Decay / Week: ${config.infractionConfiguration.pointDecayPerWeek} \n" + 40 | "Gag Duration: ${timeToString(config.infractionConfiguration.gagDuration)}" 41 | } 42 | 43 | field { 44 | name = "**Reactions**" 45 | value = "Enabled: ${config.reactions.enabled} \n" + 46 | "Gag: ${config.reactions.gagReaction} \n" + 47 | "Delete Message: ${config.reactions.deleteMessageReaction} \n" + 48 | "Flag Message: ${config.reactions.flagMessageReaction}" 49 | } 50 | 51 | field { 52 | name = "**Punishments:**" 53 | config.punishments.forEach { 54 | value += "Punishment Type: ${it.punishment} \n" + 55 | "Point Threshold: ${it.points} \n" + 56 | "Punishment Duration: ${if (it.duration !== null) timeToString(it.duration!!) else "Permanent"}" + "\n\n" 57 | } 58 | } 59 | 60 | field { 61 | name = "**Logging:**" 62 | value = "Logging Channel: ${guild.getChannelOrNull(config.loggingConfiguration.loggingChannel)?.mention} \n" + 63 | "Alert Channel: ${guild.getChannelOrNull(config.loggingConfiguration.alertChannel)?.mention}" 64 | } 65 | footer { 66 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 67 | text = guild.name 68 | } 69 | } 70 | 71 | suspend fun EmbedBuilder.createActivePunishmentsEmbed(guild: Guild, punishments: List) { 72 | title = "__Active Punishments__" 73 | color = Color.MAGENTA.kColor 74 | punishments.forEach { 75 | val user = guild.kord.getUser(Snowflake(it.userId))?.mention 76 | addField( 77 | "${it.id} - ${it.type} - ${ 78 | if (it.clearTime != null) "Cleartime - ${ 79 | TimeStamp.at( 80 | Instant.ofEpochMilli(it.clearTime), 81 | TimeStyle.RELATIVE 82 | ) 83 | }" else "" 84 | }", 85 | "User: $user" 86 | ) 87 | } 88 | footer { 89 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 90 | text = guild.name 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/embeds/InfractionEmbeds.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.embeds 2 | 3 | import dev.kord.common.kColor 4 | import dev.kord.core.entity.Guild 5 | import dev.kord.core.entity.Member 6 | import dev.kord.core.entity.Message 7 | import dev.kord.core.entity.User 8 | import dev.kord.rest.Image 9 | import dev.kord.rest.builder.message.EmbedBuilder 10 | import me.ddivad.judgebot.dataclasses.* 11 | import me.ddivad.judgebot.util.timeToString 12 | import me.jakejmattson.discordkt.extensions.addField 13 | import java.awt.Color 14 | 15 | fun EmbedBuilder.createInfractionEmbed( 16 | guild: Guild, 17 | configuration: GuildConfiguration, 18 | user: User, 19 | guildMember: GuildMember, 20 | infraction: Infraction, 21 | rule: Rule? 22 | ) { 23 | if (infraction.type == InfractionType.Warn) createWarnEmbed( 24 | guild, 25 | configuration, 26 | user, 27 | guildMember, 28 | infraction, 29 | rule 30 | ) 31 | else if (infraction.type == InfractionType.Strike) createStrikeEmbed( 32 | guild, 33 | configuration, 34 | user, 35 | guildMember, 36 | infraction, 37 | rule 38 | ) 39 | } 40 | 41 | fun EmbedBuilder.createWarnEmbed( 42 | guild: Guild, 43 | configuration: GuildConfiguration, 44 | user: User, 45 | guildMember: GuildMember, 46 | infraction: Infraction, 47 | rule: Rule? 48 | ) { 49 | title = "Warn" 50 | description = "${user.mention}, you have received a **warning** from **${guild.name}**." 51 | 52 | field { 53 | name = "__Reason__" 54 | value = infraction.reason 55 | inline = false 56 | } 57 | 58 | if (infraction.ruleNumber != null) { 59 | field { 60 | name = "__Rule Broken__" 61 | value = "**[${rule?.title}](${rule?.link})** \n${rule?.description}" 62 | } 63 | } 64 | 65 | field { 66 | name = "__Warn Points__" 67 | value = "${infraction.points}" 68 | inline = true 69 | } 70 | 71 | field { 72 | name = "__Points Count__" 73 | value = "${guildMember.getPoints(guild)} / ${configuration.infractionConfiguration.pointCeiling}" 74 | inline = true 75 | } 76 | 77 | if (infraction.punishment?.punishment != PunishmentType.NONE) { 78 | field { 79 | name = "__Punishment__" 80 | value = "${infraction.punishment?.punishment.toString()} ${ 81 | if (infraction.punishment?.duration != null) "for " + timeToString(infraction.punishment?.duration!!) else "indefinitely" 82 | }" 83 | inline = true 84 | } 85 | } 86 | 87 | addField( 88 | "", 89 | "A warning is a way for staff to inform you that your behaviour needs to change or further infractions will follow. \nIf you think this to be unjustified, please **do not** post about it in a public channel but take it up with **Modmail**." 90 | ) 91 | 92 | color = Color.RED.kColor 93 | thumbnail { 94 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 95 | } 96 | footer { 97 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 98 | text = guild.name 99 | } 100 | } 101 | 102 | fun EmbedBuilder.createStrikeEmbed( 103 | guild: Guild, 104 | configuration: GuildConfiguration, 105 | user: User, 106 | guildMember: GuildMember, 107 | infraction: Infraction, 108 | rule: Rule? 109 | ) { 110 | title = "Strike" 111 | description = "${user.mention}, you have received a **strike** from **${guild.name}**." 112 | 113 | field { 114 | name = "__Reason__" 115 | value = infraction.reason 116 | inline = false 117 | } 118 | 119 | if (infraction.ruleNumber != null) { 120 | field { 121 | name = "__Rule Broken__" 122 | value = "**[${rule?.title}](${rule?.link})** \n${rule?.description}" 123 | } 124 | } 125 | 126 | field { 127 | name = "__Strike Points__" 128 | value = "${infraction.points}" 129 | inline = true 130 | } 131 | 132 | field { 133 | name = "__Points Count__" 134 | value = "${guildMember.getPoints(guild)} / ${configuration.infractionConfiguration.pointCeiling}" 135 | inline = true 136 | } 137 | 138 | if (infraction.punishment?.punishment != PunishmentType.NONE) { 139 | field { 140 | name = "__Punishment__" 141 | value = "${infraction.punishment?.punishment.toString()} ${ 142 | if (infraction.punishment?.duration != null) "for " + timeToString(infraction.punishment?.duration!!) else "indefinitely" 143 | }" 144 | inline = true 145 | } 146 | } 147 | 148 | addField( 149 | "", 150 | " A strike is a formal warning for breaking the rules.\nIf you think this to be unjustified, please **do not** post about it in a public channel but take it up with **Modmail**." 151 | ) 152 | 153 | color = Color.RED.kColor 154 | thumbnail { 155 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 156 | } 157 | footer { 158 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 159 | text = guild.name 160 | } 161 | } 162 | 163 | fun EmbedBuilder.createMuteEmbed(guild: Guild, user: User, reason: String, length: Long) { 164 | title = "Mute" 165 | description = """ 166 | | ${user.mention}, you have been muted. A muted user cannot speak/post in channels. 167 | | If you believe this to be in error, please contact Modmail. 168 | """.trimMargin() 169 | 170 | field { 171 | name = "Length" 172 | value = timeToString(length) 173 | inline = false 174 | } 175 | 176 | field { 177 | name = "__Reason__" 178 | value = reason 179 | inline = false 180 | } 181 | color = Color.RED.kColor 182 | thumbnail { 183 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 184 | } 185 | footer { 186 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 187 | text = guild.name 188 | } 189 | } 190 | 191 | fun EmbedBuilder.createUnmuteEmbed(guild: Guild, user: User) { 192 | color = Color.GREEN.kColor 193 | title = "Mute Removed" 194 | description = "${user.mention} you have been unmuted from **${guild.name}**." 195 | footer { 196 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 197 | text = guild.name 198 | } 199 | } 200 | 201 | fun EmbedBuilder.createBadPfpEmbed(guild: Guild, user: Member) { 202 | color = Color.RED.kColor 203 | thumbnail { 204 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 205 | } 206 | title = "BadPfp" 207 | description = """ 208 | ${user.mention}, we have flagged your profile picture as inappropriate. 209 | Please change it within the next **30 minutes** or you will be banned. 210 | """.trimIndent() 211 | footer { 212 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 213 | text = guild.name 214 | } 215 | } 216 | 217 | fun EmbedBuilder.createMessageDeleteEmbed(guild: Guild, message: Message) { 218 | var messageContent = message.content.take(1010) 219 | if (message.content.length > 1024) messageContent += " ..." 220 | 221 | title = "Message Deleted" 222 | thumbnail { 223 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 224 | } 225 | color = Color.RED.kColor 226 | description = """ 227 | Your ${if (message.attachments.isNotEmpty()) "image" else "message"} was deleted from ${message.channel.mention} 228 | as it was deemed either off topic or against our server rules. 229 | """.trimIndent() 230 | addField("Message", "```${messageContent.replace("`", "")}```") 231 | if (message.attachments.isNotEmpty()) { 232 | addField("Filename", "```${message.attachments.first().filename}```") 233 | } 234 | field { 235 | value = 236 | "If you think this to be unjustified, please **do not** post about it in a public channel but take it up with Modmail." 237 | } 238 | footer { 239 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 240 | text = guild.name 241 | } 242 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/embeds/MessageEmbeds.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.embeds 2 | 3 | import dev.kord.common.kColor 4 | import dev.kord.core.entity.Guild 5 | import dev.kord.core.entity.Member 6 | import dev.kord.rest.Image 7 | import dev.kord.rest.builder.message.EmbedBuilder 8 | import me.ddivad.judgebot.dataclasses.Info 9 | import me.jakejmattson.discordkt.extensions.addField 10 | import java.awt.Color 11 | 12 | fun EmbedBuilder.createInformationEmbed(guild: Guild, user: Member, information: Info) { 13 | title = "Information" 14 | color = Color.CYAN.kColor 15 | thumbnail { 16 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 17 | } 18 | description = """ 19 | ${user.mention}, the staff of **${guild.name}** have some information they want you to read. __This is not an infraction.__ 20 | Feel free to DM **Modmail** for more clarification. 21 | """.trimMargin() 22 | 23 | addField("Message", information.message) 24 | 25 | footer { 26 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 27 | text = guild.name 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/embeds/RuleEmbeds.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.embeds 2 | 3 | import dev.kord.common.kColor 4 | import dev.kord.core.entity.Guild 5 | import dev.kord.rest.Image 6 | import dev.kord.rest.builder.message.EmbedBuilder 7 | import me.ddivad.judgebot.dataclasses.Rule 8 | import me.jakejmattson.discordkt.extensions.addField 9 | import java.awt.Color 10 | 11 | fun EmbedBuilder.createRuleEmbed(guild: Guild, rule: Rule) { 12 | title = "__${rule.number}: ${rule.title}__" 13 | description = rule.description 14 | thumbnail { 15 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 16 | } 17 | if (rule.link !== "") { 18 | addField("", "[View this on our website](${rule.link})") 19 | } 20 | color = Color.MAGENTA.kColor 21 | footer { 22 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 23 | text = guild.name 24 | } 25 | } 26 | 27 | fun EmbedBuilder.createRulesEmbed(guild: Guild, rules: List) { 28 | title = "**__Server Rules__**" 29 | thumbnail { 30 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 31 | } 32 | color = Color.MAGENTA.kColor 33 | 34 | field { 35 | for (rule in rules) { 36 | value += "**[${rule.number}](${rule.link})**. ${rule.title}\n" 37 | } 38 | } 39 | footer { 40 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 41 | text = guild.name 42 | } 43 | } 44 | 45 | fun EmbedBuilder.createRulesEmbedDetailed(guild: Guild, rules: List) { 46 | title = "**__Server Rules__**" 47 | thumbnail { 48 | url = guild.getIconUrl(Image.Format.PNG) ?: "" 49 | } 50 | color = Color.MAGENTA.kColor 51 | 52 | for (rule in rules) { 53 | field { 54 | value = if (rule.link != "") "[${rule.number}) __**${rule.title}**__](${rule.link})\n${rule.description}" 55 | else "${rule.number}) __**${rule.title}**__\n${rule.description}" 56 | inline = false 57 | } 58 | } 59 | footer { 60 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 61 | text = guild.name 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/embeds/UserEmbeds.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.embeds 2 | 3 | import dev.kord.common.entity.Snowflake 4 | import dev.kord.common.kColor 5 | import dev.kord.core.entity.Guild 6 | import dev.kord.core.entity.User 7 | import dev.kord.rest.Image 8 | import dev.kord.rest.builder.message.EmbedBuilder 9 | import dev.kord.x.emoji.Emojis 10 | import kotlinx.datetime.toJavaInstant 11 | import me.ddivad.judgebot.dataclasses.* 12 | import me.ddivad.judgebot.services.DatabaseService 13 | import me.ddivad.judgebot.util.timeToString 14 | import me.jakejmattson.discordkt.dsl.MenuBuilder 15 | import me.jakejmattson.discordkt.extensions.* 16 | import java.awt.Color 17 | import java.text.SimpleDateFormat 18 | import java.time.Instant 19 | import java.util.* 20 | 21 | suspend fun MenuBuilder.createHistoryEmbed( 22 | target: User, 23 | member: GuildMember, 24 | guild: Guild, 25 | config: Configuration, 26 | databaseService: DatabaseService 27 | ) { 28 | val userRecord = member.getGuildInfo(guild.id.toString()) 29 | val paginatedNotes = userRecord.notes.chunked(4) 30 | 31 | val totalMenuPages = 1 + 1 + 1 + 1 + if (paginatedNotes.isNotEmpty()) paginatedNotes.size else 1 32 | val guildConfiguration = config[guild.id]!! 33 | val embedColor = getEmbedColour(guild, target, databaseService) 34 | val leaveData = databaseService.joinLeaves.getMemberJoinLeaveDataForGuild(guild.id.toString(), member.userId) 35 | this.apply { 36 | buildOverviewPage(guild, guildConfiguration, target, userRecord, embedColor, totalMenuPages, databaseService) 37 | buildInfractionPage(guild, guildConfiguration, target, userRecord, embedColor, totalMenuPages) 38 | buildNotesPages(guild, guildConfiguration, target, userRecord, embedColor, totalMenuPages) 39 | buildInformationPage(guild, guildConfiguration, target, userRecord, embedColor, totalMenuPages) 40 | buildJoinLeavePage(guild, target, leaveData, userRecord, embedColor, totalMenuPages) 41 | } 42 | 43 | buttons { 44 | button( 45 | "Overview (${userRecord.points} / ${guildConfiguration.infractionConfiguration.pointCeiling})", 46 | Emojis.clipboard 47 | ) { 48 | loadPage(0) 49 | } 50 | button("Infractions (${userRecord.infractions.size})", Emojis.warning) { 51 | loadPage(1) 52 | } 53 | button("Notes (${userRecord.notes.size})", Emojis.pencil) { 54 | loadPage(2) 55 | } 56 | button("Messages (${userRecord.info.size})", Emojis.informationSource) { 57 | loadPage(3 + if (paginatedNotes.isNotEmpty()) paginatedNotes.size - 1 else 0) 58 | } 59 | button("Leaves", Emojis.x) { 60 | loadPage(4 + if (paginatedNotes.isNotEmpty()) paginatedNotes.size - 1 else 0) 61 | } 62 | } 63 | 64 | if ((paginatedNotes.size > 1)) { 65 | buttons { 66 | button("Prev.", Emojis.arrowLeft) { 67 | previousPage() 68 | } 69 | button("Next", Emojis.arrowRight) { 70 | nextPage() 71 | } 72 | } 73 | } 74 | } 75 | 76 | private suspend fun MenuBuilder.buildOverviewPage( 77 | guild: Guild, 78 | config: GuildConfiguration, 79 | target: User, 80 | userRecord: GuildMemberDetails, 81 | embedColor: Color, 82 | totalPages: Int, 83 | databaseService: DatabaseService 84 | ) { 85 | page { 86 | color = embedColor.kColor 87 | title = "User Overview (${userRecord.points} / ${config.infractionConfiguration.pointCeiling})" 88 | thumbnail { 89 | url = target.asUser().pfpUrl 90 | } 91 | val memberInGuild = target.asMemberOrNull(guild.id) 92 | 93 | addInlineField("Infractions", "**${userRecord.infractions.size}**") 94 | addInlineField("Notes", "**${userRecord.notes.size}**") 95 | addInlineField("Messages", "**${userRecord.info.size}**") 96 | addInlineField("History", "${userRecord.historyCount}") 97 | addInlineField("Created", TimeStamp.at(target.id.timestamp.toJavaInstant(), TimeStyle.RELATIVE)) 98 | 99 | if (memberInGuild != null) { 100 | addInlineField("Joined", TimeStamp.at(memberInGuild.joinedAt.toJavaInstant(), TimeStyle.RELATIVE)) 101 | } else addInlineField("", "") 102 | 103 | getStatus(guild, target, databaseService)?.let { addField("Current Status", it) } 104 | 105 | if (userRecord.infractions.size > 0) { 106 | val lastInfraction = userRecord.infractions.maxByOrNull { it.dateTime }!! 107 | addField( 108 | "**__Most Recent Infraction__**: (${ 109 | TimeStamp.at( 110 | Instant.ofEpochMilli(lastInfraction.dateTime), 111 | TimeStyle.RELATIVE 112 | ) 113 | })", 114 | "Type: **${lastInfraction.type} (${lastInfraction.points})** Rule: **${lastInfraction.ruleNumber ?: "None"}**\n " + 115 | "Issued by **${guild.kord.getUser(Snowflake(lastInfraction.moderator))?.username}** " + 116 | "on **${SimpleDateFormat("dd/MM/yyyy").format(Date(lastInfraction.dateTime))}**\n" + 117 | "Punishment: **${lastInfraction.punishment?.punishment}** ${getDurationText(lastInfraction.punishment)}\n" + 118 | lastInfraction.reason 119 | ) 120 | } else addField("", "**User has no recent infractions**") 121 | 122 | addInlineField( 123 | "", 124 | "**${userRecord.deletedMessageCount.deleteReaction}** Deletes (${config.reactions.deleteMessageReaction})" 125 | ) 126 | addInlineField("", "**${userRecord.bans.size}** Bans (${Emojis.x})") 127 | addInlineField("", "**${userRecord.linkedAccounts.size}** Alt(s) (${Emojis.link})") 128 | 129 | footer { 130 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 131 | text = "Page 1 of $totalPages" 132 | } 133 | } 134 | } 135 | 136 | private suspend fun MenuBuilder.buildInfractionPage( 137 | guild: Guild, 138 | config: GuildConfiguration, 139 | target: User, 140 | userRecord: GuildMemberDetails, 141 | embedColor: Color, 142 | totalPages: Int 143 | ) { 144 | page { 145 | color = embedColor.kColor 146 | title = "Infractions" 147 | thumbnail { 148 | url = target.asUser().pfpUrl 149 | } 150 | val warnings = userRecord.infractions.filter { it.type == InfractionType.Warn }.sortedBy { it.dateTime } 151 | val strikes = userRecord.infractions.filter { it.type == InfractionType.Strike }.sortedBy { it.dateTime } 152 | 153 | addInlineField("Current Points", "**${userRecord.points} / ${config.infractionConfiguration.pointCeiling}**") 154 | addInlineField("Infraction Count", "${userRecord.infractions.size}") 155 | addInlineField("Total Points", "${userRecord.infractions.sumOf { it.points }}") 156 | 157 | 158 | if (userRecord.infractions.isEmpty()) addField("", "**User has no infractions**") 159 | if (warnings.isNotEmpty()) addField("", "**__Warnings (${warnings.size})__**") 160 | warnings.forEachIndexed { _, infraction -> 161 | val moderator = guild.kord.getUser(Snowflake(infraction.moderator))?.username 162 | addField( 163 | "ID :: ${infraction.id} :: Staff :: __${moderator}__", 164 | "Type: **${infraction.type} (${infraction.points})** :: " + 165 | "Rule: **${infraction.ruleNumber ?: "None"}**\n" + 166 | "Date: **${SimpleDateFormat("dd/MM/yyyy").format(Date(infraction.dateTime))}** (${ 167 | TimeStamp.at( 168 | Instant.ofEpochMilli(infraction.dateTime), 169 | TimeStyle.RELATIVE 170 | ) 171 | })\n " + 172 | "Punishment: **${infraction.punishment?.punishment}** ${getDurationText(infraction.punishment)}\n" + 173 | infraction.reason 174 | ) 175 | } 176 | 177 | if (strikes.isNotEmpty()) addField("", "**__Strikes (${strikes.size})__**") 178 | strikes.forEachIndexed { _, infraction -> 179 | val moderator = guild.kord.getUser(Snowflake(infraction.moderator))?.username 180 | addField( 181 | "ID :: ${infraction.id} :: Staff :: __${moderator}__", 182 | "Type: **${infraction.type} (${infraction.points})** :: " + 183 | "Rule: **${infraction.ruleNumber ?: "None"}**\n" + 184 | 185 | "Date: **${SimpleDateFormat("dd/MM/yyyy").format(Date(infraction.dateTime))}** (${ 186 | TimeStamp.at( 187 | Instant.ofEpochMilli(infraction.dateTime), 188 | TimeStyle.RELATIVE 189 | ) 190 | })\n " + 191 | "Punishment: **${infraction.punishment?.punishment}** ${getDurationText(infraction.punishment)}\n" + 192 | infraction.reason 193 | ) 194 | } 195 | 196 | footer { 197 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 198 | text = "Page 2 of $totalPages" 199 | } 200 | } 201 | } 202 | 203 | private suspend fun MenuBuilder.buildNotesPages( 204 | guild: Guild, 205 | config: GuildConfiguration, 206 | target: User, 207 | userRecord: GuildMemberDetails, 208 | embedColor: Color, 209 | totalPages: Int 210 | ) { 211 | val paginatedNotes = userRecord.notes.sortedBy { it.dateTime }.chunked(4) 212 | if (userRecord.notes.isEmpty()) { 213 | page { 214 | color = embedColor.kColor 215 | title = "Notes" 216 | thumbnail { 217 | url = target.asUser().pfpUrl 218 | } 219 | 220 | addInlineField("Points", "**${userRecord.points} / ${config.infractionConfiguration.pointCeiling}**") 221 | addInlineField("Notes", "${userRecord.notes.size}") 222 | addInlineField("Messages", "${userRecord.info.size}") 223 | 224 | addField("", "**User has no notes**") 225 | footer { 226 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 227 | text = "Page 3 of $totalPages" 228 | } 229 | } 230 | } 231 | 232 | paginatedNotes.forEachIndexed { index, list -> 233 | page { 234 | color = embedColor.kColor 235 | title = "Notes" + if (paginatedNotes.size > 1) "(${index + 1})" else "" 236 | thumbnail { 237 | url = target.asUser().pfpUrl 238 | } 239 | addInlineField("Notes", "${userRecord.notes.size}") 240 | addInlineField("Messages", "${userRecord.info.size}") 241 | addInlineField("Points", "**${userRecord.points} / ${config.infractionConfiguration.pointCeiling}**") 242 | 243 | list.forEachIndexed { _, note -> 244 | val moderator = guild.kord.getUser(Snowflake(note.moderator))?.username ?: note.moderator 245 | addField( 246 | "ID :: ${note.id} :: Staff :: __${moderator}__", 247 | "Date: **${SimpleDateFormat("dd/MM/yyyy").format(Date(note.dateTime))}** (${ 248 | TimeStamp.at( 249 | Instant.ofEpochMilli( 250 | note.dateTime 251 | ), TimeStyle.RELATIVE 252 | ) 253 | })\n" + 254 | note.note 255 | ) 256 | } 257 | footer { 258 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 259 | text = "Page ${3 + index} of $totalPages" 260 | } 261 | } 262 | } 263 | } 264 | 265 | private suspend fun MenuBuilder.buildInformationPage( 266 | guild: Guild, 267 | config: GuildConfiguration, 268 | target: User, 269 | userRecord: GuildMemberDetails, 270 | embedColor: Color, 271 | totalPages: Int 272 | ) { 273 | val paginatedNotes = userRecord.notes.chunked(4) 274 | page { 275 | color = embedColor.kColor 276 | title = "Messages" 277 | thumbnail { 278 | url = target.asUser().pfpUrl 279 | } 280 | addInlineField("Notes", "${userRecord.notes.size}") 281 | addInlineField("Messages", "${userRecord.info.size}") 282 | addInlineField("Points", "**${userRecord.points} / ${config.infractionConfiguration.pointCeiling}**") 283 | 284 | if (userRecord.info.isEmpty()) addField("", "**User has no message records**") 285 | userRecord.info.forEachIndexed { _, info -> 286 | val moderator = guild.kord.getUser(Snowflake(info.moderator))?.username 287 | addField( 288 | "ID :: ${info.id} :: Staff :: __${moderator}__", 289 | "Date: **${SimpleDateFormat("dd/MM/yyyy").format(Date(info.dateTime))}** (${ 290 | TimeStamp.at( 291 | Instant.ofEpochMilli( 292 | info.dateTime 293 | ), TimeStyle.RELATIVE 294 | ) 295 | })\n" + 296 | info.message 297 | ) 298 | } 299 | footer { 300 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 301 | text = "Page ${3 + if (paginatedNotes.isEmpty()) 1 else paginatedNotes.size} of $totalPages" 302 | } 303 | } 304 | } 305 | 306 | private suspend fun MenuBuilder.buildJoinLeavePage( 307 | guild: Guild, 308 | target: User, 309 | joinLeaves: List, 310 | userRecord: GuildMemberDetails, 311 | embedColor: Color, 312 | totalPages: Int 313 | ) { 314 | page { 315 | val leaves = joinLeaves.filter { it.leaveDate != null } 316 | val paginatedNotes = userRecord.notes.chunked(4) 317 | 318 | color = embedColor.kColor 319 | title = "Join / Leave" 320 | thumbnail { 321 | url = target.asUser().pfpUrl 322 | } 323 | 324 | addInlineField("Joins:", joinLeaves.size.toString()) 325 | addInlineField("", "") 326 | addInlineField("Leaves:", leaves.size.toString()) 327 | addField("", "") 328 | joinLeaves.forEachIndexed { index, record -> 329 | addInlineField("Record", "#${index + 1}") 330 | addInlineField("Joined", TimeStamp.at(Instant.ofEpochMilli(record.joinDate), TimeStyle.DATE_SHORT)) 331 | addInlineField( 332 | "Left", if (record.leaveDate == null) "-" else TimeStamp.at(Instant.ofEpochMilli(record.leaveDate!!), TimeStyle.DATE_SHORT) 333 | ) 334 | 335 | } 336 | footer { 337 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 338 | text = "Page ${4 + if (paginatedNotes.isEmpty()) 1 else paginatedNotes.size} of $totalPages" 339 | } 340 | } 341 | } 342 | 343 | private fun getDurationText(level: PunishmentLevel?): String { 344 | if (level == null) return "" 345 | return when { 346 | level.punishment == PunishmentType.NONE -> { 347 | "" 348 | } 349 | level.duration != null -> { 350 | "for **" + timeToString(level.duration!!) + "**" 351 | } 352 | else -> { 353 | "" 354 | } 355 | } 356 | } 357 | 358 | private suspend fun getEmbedColour(guild: Guild, target: User, databaseService: DatabaseService): Color { 359 | if (guild.getBanOrNull(target.id) != null) return Color.RED 360 | if (target.asMemberOrNull(guild.id) == null) return Color.BLACK 361 | if (databaseService.guilds.getPunishmentsForUser(guild, target).isNotEmpty()) return Color.ORANGE 362 | return Color.MAGENTA 363 | } 364 | 365 | private suspend fun getStatus(guild: Guild, target: User, databaseService: DatabaseService): String? { 366 | var status = "" 367 | val userRecord = databaseService.users.getOrCreateUser(target, guild).getGuildInfo(guild.id.toString()) 368 | guild.getBanOrNull(target.id)?.let { 369 | val reason = databaseService.guilds.getBanOrNull(guild, target.id.toString())?.reason ?: it.reason 370 | return "```css\nUser is banned with reason:\n${reason}```" 371 | } 372 | if (target.asMemberOrNull(guild.id) == null) return "```css\nUser not currently in this guild```" 373 | if (userRecord.pointDecayFrozen) { 374 | status += "```css\nPoint decay is currently frozen for this user```" 375 | } 376 | if (userRecord.bans.lastOrNull()?.thinIce == true && userRecord.pointDecayFrozen && Instant.now() 377 | .toEpochMilli() < userRecord.pointDecayTimer 378 | ) { 379 | status += "User is on Thin Ice after being unbanned on ${ 380 | userRecord.bans.last().unbanTime?.let { 381 | Instant.ofEpochMilli( 382 | it 383 | ) 384 | }?.let { TimeStamp.at(it) } 385 | }. Point decay frozen until ${TimeStamp.at(Instant.ofEpochMilli(userRecord.pointDecayTimer), TimeStyle.DATETIME_SHORT)}" 386 | } 387 | databaseService.guilds.getPunishmentsForUser(guild, target).firstOrNull()?.let { 388 | val clearTime = Instant.ofEpochMilli(it.clearTime!!) 389 | status += "\nMuted until ${TimeStamp.at(clearTime, TimeStyle.DATETIME_SHORT)} (${TimeStamp.at(clearTime, TimeStyle.RELATIVE)})" 390 | } 391 | return status 392 | } 393 | 394 | suspend fun MenuBuilder.createLinkedAccountMenu( 395 | linkedAccountIds: List, 396 | guild: Guild, 397 | config: Configuration, 398 | databaseService: DatabaseService 399 | ) { 400 | linkedAccountIds.forEach { 401 | val linkedUser = guild.kord.getUser(it.toSnowflake()) ?: return@forEach 402 | val linkedUserRecord = databaseService.users.getOrCreateUser(linkedUser, guild) 403 | page { 404 | createCondensedHistoryEmbed(linkedUser, linkedUserRecord, guild, config) 405 | } 406 | } 407 | 408 | buttons { 409 | button("Prev.", Emojis.arrowLeft) { 410 | previousPage() 411 | } 412 | button("Next", Emojis.arrowRight) { 413 | nextPage() 414 | } 415 | } 416 | } 417 | 418 | suspend fun EmbedBuilder.createCondensedHistoryEmbed( 419 | target: User, 420 | member: GuildMember, 421 | guild: Guild, 422 | config: Configuration 423 | ) { 424 | 425 | val userGuildDetails = member.getGuildInfo(guild.id.toString()) 426 | val infractions = userGuildDetails.infractions 427 | val warnings = userGuildDetails.infractions.filter { it.type == InfractionType.Warn } 428 | val strikes = userGuildDetails.infractions.filter { it.type == InfractionType.Strike } 429 | val notes = userGuildDetails.notes 430 | val maxPoints = config[guild.id]?.infractionConfiguration?.pointCeiling 431 | 432 | color = Color.MAGENTA.kColor 433 | title = "User Overview (${userGuildDetails.points} / $maxPoints)" 434 | thumbnail { 435 | url = target.asUser().pfpUrl 436 | } 437 | val memberInGuild = target.asMemberOrNull(guild.id) 438 | 439 | addInlineField("Infractions", "**${userGuildDetails.infractions.size}**") 440 | addInlineField("Notes", "**${userGuildDetails.notes.size}**") 441 | addInlineField("Info", "**${userGuildDetails.info.size}**") 442 | addInlineField("History", "${userGuildDetails.historyCount}") 443 | addInlineField("Created", TimeStamp.at(target.id.timestamp.toJavaInstant(), TimeStyle.RELATIVE)) 444 | 445 | if (memberInGuild != null) { 446 | addInlineField("Joined", TimeStamp.at(memberInGuild.joinedAt.toJavaInstant(), TimeStyle.RELATIVE)) 447 | } else addInlineField("", "") 448 | 449 | if (notes.isEmpty()) { 450 | addField("", "**__Notes__**") 451 | addField("No notes recorded.", "") 452 | } else { 453 | addField("", "**__Notes__**") 454 | notes.forEach { note -> 455 | val moderator = guild.kord.getUser(Snowflake(note.moderator))?.username 456 | 457 | addField( 458 | "ID :: ${note.id} :: Staff :: __${moderator}__", 459 | "Noted by **${moderator}** on **${SimpleDateFormat("dd/MM/yyyy").format(Date(note.dateTime))}** (${ 460 | TimeStamp.at( 461 | Instant.ofEpochMilli(note.dateTime), 462 | TimeStyle.RELATIVE 463 | ) 464 | })\n" + 465 | note.note 466 | ) 467 | } 468 | } 469 | 470 | if (infractions.isEmpty()) { 471 | addField("", "**__Infractions__**") 472 | addField("No infractions issued.", "") 473 | } else { 474 | if (warnings.isNotEmpty()) addField("", "**__Warnings__**") 475 | warnings.forEachIndexed { index, infraction -> 476 | val moderator = guild.kord.getUser(Snowflake(infraction.moderator))?.username 477 | addField( 478 | "ID :: $index :: Staff :: $moderator", 479 | "Type: **${infraction.type} (${infraction.points})** :: " + 480 | "Date: **${SimpleDateFormat("dd/MM/yyyy").format(Date(infraction.dateTime))}** (${ 481 | TimeStamp.at( 482 | Instant.ofEpochMilli(infraction.dateTime), 483 | TimeStyle.RELATIVE 484 | ) 485 | })\n " + 486 | "Punishment: **${infraction.punishment?.punishment}** ${ 487 | if (infraction.punishment?.duration != null && infraction.punishment?.punishment !== PunishmentType.NONE) 488 | "for **" + timeToString(infraction.punishment?.duration!!) + "**" else "" 489 | }\n" + 490 | infraction.reason 491 | ) 492 | } 493 | 494 | if (strikes.isNotEmpty()) addField("", "**__Strikes__**") 495 | strikes.forEachIndexed { index, infraction -> 496 | val moderator = guild.kord.getUser(Snowflake(infraction.moderator))?.username 497 | addField( 498 | "ID :: $index :: Staff :: $moderator", 499 | "Type: **${infraction.type} (${infraction.points})** :: " + 500 | "Date: **${SimpleDateFormat("dd/MM/yyyy").format(Date(infraction.dateTime))}** (${ 501 | TimeStamp.at( 502 | Instant.ofEpochMilli(infraction.dateTime), 503 | TimeStyle.RELATIVE 504 | ) 505 | })\n " + 506 | "Punishment: **${infraction.punishment?.punishment}** ${ 507 | if (infraction.punishment?.duration != null && infraction.punishment?.punishment !== PunishmentType.NONE) 508 | "for **" + timeToString(infraction.punishment?.duration!!) + "**" else "" 509 | }\n" + 510 | infraction.reason 511 | ) 512 | } 513 | } 514 | 515 | footer { 516 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 517 | text = guild.name 518 | } 519 | } 520 | 521 | suspend fun EmbedBuilder.createSelfHistoryEmbed( 522 | target: User, 523 | member: GuildMember, 524 | guild: Guild, 525 | config: Configuration 526 | ) { 527 | val userGuildDetails = member.getGuildInfo(guild.id.toString()) 528 | val infractions = userGuildDetails.infractions 529 | val warnings = userGuildDetails.infractions.filter { it.type == InfractionType.Warn } 530 | val strikes = userGuildDetails.infractions.filter { it.type == InfractionType.Strike } 531 | val maxPoints = config[guild.id]?.infractionConfiguration?.pointCeiling 532 | 533 | color = Color.MAGENTA.kColor 534 | title = "${target.asUser().tag}'s Record" 535 | thumbnail { 536 | url = target.asUser().pfpUrl 537 | } 538 | addInlineField("Infractions", "${infractions.size}") 539 | addInlineField("Points", "**${member.getPoints(guild)} / $maxPoints**") 540 | 541 | if (infractions.isEmpty()) { 542 | addField("", "") 543 | addField("No infractions issued.", "") 544 | } else { 545 | if (warnings.isNotEmpty()) addField("", "**__Warnings__**") 546 | warnings.forEachIndexed { index, infraction -> 547 | addField( 548 | "ID :: $index :: Weight :: ${infraction.points}", 549 | "Type: **${infraction.type} (${infraction.points})** :: " + 550 | "Date: **${SimpleDateFormat("dd/MM/yyyy").format(Date(infraction.dateTime))}** (${ 551 | TimeStamp.at( 552 | Instant.ofEpochMilli(infraction.dateTime), TimeStyle.RELATIVE 553 | ) 554 | })\n " + 555 | "Punishment: **${infraction.punishment?.punishment}** ${ 556 | if (infraction.punishment?.duration != null && infraction.punishment?.punishment !== PunishmentType.NONE) 557 | "for **" + timeToString(infraction.punishment?.duration!!) + "**" else "" 558 | }\n" + 559 | infraction.reason 560 | ) 561 | } 562 | 563 | if (strikes.isNotEmpty()) addField("", "**__Strikes__**") 564 | strikes.forEachIndexed { index, infraction -> 565 | addField( 566 | "ID :: $index :: Weight :: ${infraction.points}", 567 | "Type: **${infraction.type} (${infraction.points})** :: " + 568 | "Date: **${SimpleDateFormat("dd/MM/yyyy").format(Date(infraction.dateTime))}** (${ 569 | TimeStamp.at( 570 | Instant.ofEpochMilli(infraction.dateTime), TimeStyle.RELATIVE 571 | ) 572 | })\n " + 573 | "Punishment: **${infraction.punishment?.punishment}** ${ 574 | if (infraction.punishment?.duration != null && infraction.punishment?.punishment !== PunishmentType.NONE) 575 | "for **" + timeToString(infraction.punishment?.duration!!) + "**" else "" 576 | }\n" + 577 | infraction.reason 578 | ) 579 | } 580 | } 581 | 582 | footer { 583 | icon = guild.getIconUrl(Image.Format.PNG) ?: "" 584 | text = guild.name 585 | } 586 | } 587 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/extensions/Kord.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.extensions 2 | 3 | import dev.kord.core.entity.Member 4 | import dev.kord.core.entity.User 5 | import kotlinx.coroutines.flow.toList 6 | import me.ddivad.judgebot.dataclasses.GuildConfiguration 7 | 8 | suspend fun User.testDmStatus() { 9 | getDmChannel().createMessage("Infraction message incoming").delete() 10 | } 11 | 12 | suspend fun Member.getHighestRolePosition(): Int { 13 | if (isBot) return -1 14 | return roles.toList().maxByOrNull { it.rawPosition }?.rawPosition ?: -1 15 | } 16 | 17 | fun Member.hasStaffRoles(guildConfiguration: GuildConfiguration): Boolean { 18 | val staffRoleIds = 19 | ((guildConfiguration.adminRoles union guildConfiguration.staffRoles) union guildConfiguration.moderatorRoles) 20 | return staffRoleIds.any { roleIds.contains(it) } 21 | } 22 | 23 | fun Member.hasAdminRoles(guildConfiguration: GuildConfiguration): Boolean { 24 | return guildConfiguration.adminRoles.any { roleIds.contains(it) } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/listeners/JoinLeaveListener.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.listeners 2 | 3 | import dev.kord.core.event.guild.MemberJoinEvent 4 | import dev.kord.core.event.guild.MemberLeaveEvent 5 | import kotlinx.coroutines.GlobalScope 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.launch 8 | import me.ddivad.judgebot.services.DatabaseService 9 | import me.jakejmattson.discordkt.dsl.listeners 10 | 11 | @Suppress("unused") 12 | fun onGuildMemberLeave(databaseService: DatabaseService) = listeners { 13 | on { 14 | databaseService.joinLeaves.addLeaveData(guildId.toString(), user.id.toString()) 15 | } 16 | 17 | on { 18 | // Add delay before creating user in case they are banned immediately (raid, etc...) 19 | GlobalScope.launch { 20 | delay(1000 * 10 * 1) 21 | guild.getMemberOrNull(member.id)?.let { 22 | databaseService.joinLeaves.createJoinLeaveRecord(guildId.toString(), member) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/listeners/MemberReactionListeners.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.listeners 2 | 3 | import dev.kord.core.behavior.getChannelOf 4 | import dev.kord.core.entity.channel.TextChannel 5 | import dev.kord.core.event.message.ReactionAddEvent 6 | import dev.kord.x.emoji.Emojis 7 | import dev.kord.x.emoji.addReaction 8 | import me.ddivad.judgebot.dataclasses.Configuration 9 | import me.ddivad.judgebot.util.createFlagMessage 10 | import me.jakejmattson.discordkt.dsl.listeners 11 | 12 | @Suppress("unused") 13 | fun onMemberReactionAdd(configuration: Configuration) = listeners { 14 | on { 15 | val guild = guild?.asGuildOrNull() ?: return@on 16 | val guildConfiguration = configuration[guild.asGuild().id] 17 | if (!guildConfiguration?.reactions!!.enabled) return@on 18 | 19 | when (this.emoji.name) { 20 | guildConfiguration.reactions.flagMessageReaction -> { 21 | message.deleteReaction(this.emoji) 22 | val channel = message.getChannel() 23 | guild.asGuild() 24 | .getChannelOf(guildConfiguration.loggingConfiguration.alertChannel) 25 | .asChannel() 26 | .createMessage(createFlagMessage(user.asUser(), message.asMessage(), channel)) 27 | .addReaction(Emojis.question) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/listeners/NewChannelOverrideListener.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.listeners 2 | 3 | import dev.kord.common.entity.Permission 4 | import dev.kord.common.entity.Permissions 5 | import dev.kord.core.entity.PermissionOverwrite 6 | import dev.kord.core.event.channel.TextChannelCreateEvent 7 | import me.ddivad.judgebot.dataclasses.Configuration 8 | import me.ddivad.judgebot.services.LoggingService 9 | import me.jakejmattson.discordkt.dsl.listeners 10 | 11 | @Suppress("unused") 12 | fun onChannelCreated(configuration: Configuration, loggingService: LoggingService) = listeners { 13 | on { 14 | val channel = this.channel 15 | val guild = channel.getGuild() 16 | val guildConfiguration = configuration[guild.id] ?: return@on 17 | val mutedRole = guild.getRole(guildConfiguration.mutedRole) 18 | val deniedPermissions = channel.getPermissionOverwritesForRole(mutedRole.id)?.denied ?: Permissions() 19 | if (deniedPermissions.values.any { 20 | it in setOf( 21 | Permission.SendMessages, 22 | Permission.AddReactions, 23 | Permission.CreatePublicThreads, 24 | Permission.CreatePrivateThreads, 25 | Permission.SendMessagesInThreads 26 | ) 27 | }) { 28 | channel.addOverwrite( 29 | PermissionOverwrite.forRole( 30 | mutedRole.id, 31 | denied = deniedPermissions.plus(Permission.SendMessages).plus(Permission.AddReactions) 32 | .plus(Permission.CreatePrivateThreads).plus(Permission.CreatePrivateThreads) 33 | .plus(Permission.SendMessagesInThreads) 34 | ), 35 | "Judgebot Overwrite" 36 | ) 37 | loggingService.channelOverrideAdded(guild, channel) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/listeners/RejoinMuteListener.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.listeners 2 | 3 | import dev.kord.core.event.guild.MemberJoinEvent 4 | import me.ddivad.judgebot.services.LoggingService 5 | import me.ddivad.judgebot.services.infractions.MuteService 6 | import me.ddivad.judgebot.services.infractions.MuteState 7 | import me.jakejmattson.discordkt.dsl.listeners 8 | 9 | @Suppress("unused") 10 | fun onMemberRejoinWithMute(muteService: MuteService, loggingService: LoggingService) = listeners { 11 | on { 12 | val member = this.member 13 | val guild = this.getGuild() 14 | if (muteService.checkMuteState(guild, member) == MuteState.Tracked) { 15 | loggingService.rejoinMute(guild, member.asUser(), MuteState.Tracked) 16 | muteService.handleRejoinMute(guild, member) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/listeners/StaffReactionListeners.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.listeners 2 | 3 | import dev.kord.common.exception.RequestException 4 | import dev.kord.core.event.message.ReactionAddEvent 5 | import dev.kord.x.emoji.Emojis 6 | import dev.kord.x.emoji.addReaction 7 | import me.ddivad.judgebot.dataclasses.Configuration 8 | import me.ddivad.judgebot.embeds.createMessageDeleteEmbed 9 | import me.ddivad.judgebot.extensions.getHighestRolePosition 10 | import me.ddivad.judgebot.extensions.hasStaffRoles 11 | import me.ddivad.judgebot.services.LoggingService 12 | import me.ddivad.judgebot.services.infractions.InfractionService 13 | import me.ddivad.judgebot.services.infractions.MuteService 14 | import me.ddivad.judgebot.services.infractions.MuteState 15 | import me.jakejmattson.discordkt.dsl.listeners 16 | import me.jakejmattson.discordkt.extensions.isSelf 17 | import me.jakejmattson.discordkt.extensions.sendPrivateMessage 18 | 19 | @Suppress("unused") 20 | fun onStaffReactionAdd( 21 | muteService: MuteService, 22 | infractionService: InfractionService, 23 | loggingService: LoggingService, 24 | configuration: Configuration 25 | ) = listeners { 26 | on { 27 | val guild = guild?.asGuildOrNull() ?: return@on 28 | val guildConfiguration = configuration[guild.asGuild().id] 29 | if (!guildConfiguration?.reactions!!.enabled) return@on 30 | val reactionUser = user.asMemberOrNull(guild.id) ?: return@on 31 | val msg = message.asMessage() 32 | val messageAuthor = msg.author?.asMemberOrNull(guild.id) ?: return@on 33 | 34 | if (reactionUser.hasStaffRoles(guildConfiguration) && reactionUser.getHighestRolePosition() > messageAuthor.getHighestRolePosition()) { 35 | when (this.emoji.name) { 36 | guildConfiguration.reactions.gagReaction -> { 37 | loggingService.staffReactionUsed(guild, reactionUser, messageAuthor, this.emoji) 38 | msg.deleteReaction(this.emoji) 39 | if (muteService.checkMuteState(guild, messageAuthor) == MuteState.Tracked) { 40 | reactionUser.sendPrivateMessage("${messageAuthor.mention} is already muted.") 41 | return@on 42 | } 43 | muteService.gag(guild, messageAuthor, reactionUser) 44 | reactionUser.sendPrivateMessage("${messageAuthor.mention} gagged.") 45 | } 46 | guildConfiguration.reactions.deleteMessageReaction -> { 47 | infractionService.deleteMessage(guild, messageAuthor, msg, reactionUser) 48 | try { 49 | messageAuthor.sendPrivateMessage { 50 | createMessageDeleteEmbed(guild, msg) 51 | } 52 | } catch (ex: RequestException) { 53 | reactionUser.sendPrivateMessage( 54 | "User ${messageAuthor.mention} has DM's disabled." + 55 | " Message deleted without notification." 56 | ) 57 | } 58 | } 59 | Emojis.question.unicode -> { 60 | if (this.user.isSelf() || msg.author != this.message.kord.getSelf()) return@on 61 | msg.deleteReaction(this.emoji) 62 | msg.addReaction(Emojis.whiteCheckMark) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/preconditions/CommandLogger.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.preconditions 2 | 3 | import dev.kord.core.entity.channel.GuildChannel 4 | import me.ddivad.judgebot.dataclasses.Configuration 5 | import me.jakejmattson.discordkt.dsl.precondition 6 | import me.jakejmattson.discordkt.extensions.idDescriptor 7 | import mu.KotlinLogging 8 | 9 | val logger = KotlinLogging.logger { } 10 | 11 | @Suppress("unused") 12 | fun commandLogger(configuration: Configuration) = precondition { 13 | command ?: return@precondition 14 | if (guild != null) { 15 | val guild = guild!! 16 | val channel = channel as GuildChannel 17 | val message = "${author.idDescriptor()} Invoked `${command!!.names.first()}` in #${channel.name}." 18 | logger.info { "${guild.name} (${guild.id}): $message" } 19 | } 20 | 21 | return@precondition 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/preconditions/PrefixPrecondition.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.preconditions 2 | 3 | import me.ddivad.judgebot.dataclasses.Configuration 4 | import me.jakejmattson.discordkt.dsl.precondition 5 | 6 | @Suppress("unused") 7 | fun prefixPrecondition(configuration: Configuration) = precondition { 8 | if (guild == null) return@precondition 9 | if (message == null) return@precondition 10 | if (author.isBot) return@precondition 11 | 12 | val guildConfig = configuration[guild!!.id] ?: return@precondition 13 | val content = message!!.content 14 | 15 | if (content.startsWith(guildConfig.prefix)) { 16 | fail("Text commands are deprecated. Please use the appropriate slash command.") 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/DatabaseService.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services 2 | 3 | import me.ddivad.judgebot.services.database.* 4 | import me.jakejmattson.discordkt.annotations.Service 5 | 6 | @Service 7 | open class DatabaseService( 8 | val users: UserOperations, 9 | val guilds: GuildOperations, 10 | val joinLeaves: JoinLeaveOperations, 11 | val meta: MetaOperations, 12 | val messageDeletes: MessageDeleteOperations 13 | ) -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/LoggingService.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services 2 | 3 | import dev.kord.common.entity.Snowflake 4 | import dev.kord.core.behavior.getChannelOf 5 | import dev.kord.core.entity.* 6 | import dev.kord.core.entity.channel.TextChannel 7 | import me.ddivad.judgebot.dataclasses.Ban 8 | import me.ddivad.judgebot.dataclasses.Configuration 9 | import me.ddivad.judgebot.dataclasses.GuildMember 10 | import me.ddivad.judgebot.dataclasses.Infraction 11 | import me.ddivad.judgebot.services.infractions.MuteState 12 | import me.jakejmattson.discordkt.annotations.Service 13 | import me.jakejmattson.discordkt.extensions.descriptor 14 | import me.jakejmattson.discordkt.extensions.idDescriptor 15 | import me.jakejmattson.discordkt.extensions.pfpUrl 16 | import mu.KotlinLogging 17 | 18 | @Service 19 | class LoggingService(private val configuration: Configuration) { 20 | val logger = KotlinLogging.logger { } 21 | 22 | suspend fun roleApplied(guild: Guild, user: User, role: Role) { 23 | log(guild, "**Info ::** Role ${role.mention} :: ${role.id.value} added to ${user.mention} :: ${user.tag}") 24 | logger.info { buildGuildLogMessage(guild,"Role ${role.name} added to ${user.idDescriptor()}") } 25 | } 26 | 27 | suspend fun muteOverwritten(guild: Guild, user: User) = 28 | log( 29 | guild, 30 | "**Info ::** User ${user.mention} :: ${user.tag} had an active mute, but has received another mute. Active mute will be replaced." 31 | ) 32 | 33 | suspend fun roleRemoved(guild: Guild, user: User, role: Role) { 34 | log(guild, "**Info ::** Role ${role.mention} :: ${role.id.value} removed from ${user.mention} :: ${user.tag}") 35 | logger.info { buildGuildLogMessage(guild,"Role ${role.name} removed from ${user.idDescriptor()}") } 36 | } 37 | 38 | suspend fun rejoinMute(guild: Guild, user: User, roleState: MuteState) = 39 | log( 40 | guild, 41 | "**Info ::** User ${user.mention} :: ${user.tag} joined the server with ${if (roleState == MuteState.Tracked) "an infraction" else "a manual"} mute remaining" 42 | ) 43 | 44 | suspend fun channelOverrideAdded(guild: Guild, channel: TextChannel) { 45 | log(guild, "**Info ::** Channel overrides for muted role added to ${channel.name}") 46 | logger.info { buildGuildLogMessage(guild, "Channel override for muted role added to ${channel.name}") } 47 | } 48 | 49 | suspend fun userBanned(guild: Guild, user: User, ban: Ban) { 50 | val moderator = guild.kord.getUser(Snowflake(ban.moderator)) 51 | log( 52 | guild, 53 | "**Info ::** User ${user.mention} :: ${user.tag} banned by ${moderator?.username} for reason: ${ban.reason}" 54 | ) 55 | logger.info { buildGuildLogMessage(guild, "${user.idDescriptor()} banned by ${moderator?.idDescriptor()}") } 56 | } 57 | 58 | suspend fun userUnbanned(guild: Guild, user: User) { 59 | log(guild, "**Info ::** User ${user.mention} :: ${user.tag} unbanned") 60 | logger.info { buildGuildLogMessage(guild, "${user.idDescriptor()} unbanned") } 61 | } 62 | 63 | 64 | suspend fun infractionApplied(guild: Guild, user: User, infraction: Infraction) { 65 | val moderator = guild.kord.getUser(Snowflake(infraction.moderator)) 66 | log( 67 | guild, 68 | "**Info ::** User ${user.mention} :: ${user.tag} was infracted (**${infraction.type}**) by **${moderator?.username} :: ${moderator?.tag}** \n**Reason**: ${infraction.reason}" 69 | ) 70 | logger.info { buildGuildLogMessage(guild, "${user.idDescriptor()} infracted (${infraction.type}) by ${moderator?.idDescriptor()}") } 71 | } 72 | 73 | suspend fun badBfpApplied(guild: Guild, user: Member) { 74 | log(guild, "**Info ::** User ${user.mention} badPfp triggered for avatar <${user.pfpUrl}>") 75 | logger.info { buildGuildLogMessage(guild, "BadPfP triggered for ${user.idDescriptor()} with ${user.pfpUrl}") } 76 | } 77 | 78 | suspend fun badPfpCancelled(guild: Guild, user: Member) { 79 | log(guild, "**Info ::** BadPfp cancelled for user ${user.mention}") 80 | logger.info { buildGuildLogMessage(guild, "BadPfP cancelled for ${user.idDescriptor()}") } 81 | } 82 | 83 | suspend fun badPfpBan(guild: Guild, user: Member) { 84 | log(guild, "**Info ::** User ${user.mention} banned for not changing their avatar") 85 | logger.info { buildGuildLogMessage(guild, "User ${user.idDescriptor()} banned for not changing their avatar (${user.pfpUrl})") } 86 | } 87 | 88 | suspend fun initialiseMutes(guild: Guild, role: Role) { 89 | log(guild, "**Info ::** Existing mute timers initialized using ${role.mention} :: ${role.id.value}") 90 | logger.info { buildGuildLogMessage(guild, "Existing mute timers initialized using ${role.name} (${role.id})") } 91 | } 92 | 93 | suspend fun dmDisabled(guild: Guild, target: User) = 94 | log( 95 | guild, 96 | "**Info ::** Attempted to send direct message to ${target.mention} :: ${target.id.value} but they have DMs disabled" 97 | ) 98 | 99 | suspend fun gagApplied(guild: Guild, target: Member, moderator: User) { 100 | log( 101 | guild, 102 | "**Info ::** User ${target.mention} has been gagged by **${moderator.username} :: ${moderator.tag}**" 103 | ) 104 | logger.info { buildGuildLogMessage(guild, "${target.idDescriptor()} gagged by ${moderator.idDescriptor()}") } 105 | } 106 | 107 | 108 | suspend fun staffReactionUsed(guild: Guild, moderator: User, target: Member, reaction: ReactionEmoji) { 109 | log(guild, "**Info ::** ${reaction.name} used by ${moderator.username} on ${target.mention}") 110 | logger.info { buildGuildLogMessage(guild, "Reaction ${reaction.name} used by ${moderator.idDescriptor()}") } 111 | } 112 | 113 | suspend fun deleteReactionUsed( 114 | guild: Guild, 115 | moderator: User, 116 | target: Member, 117 | reaction: ReactionEmoji, 118 | message: Message 119 | ): List { 120 | logger.info { buildGuildLogMessage(guild, "Reaction ${reaction.name} used by ${moderator.idDescriptor()}") } 121 | val msg = message.content.chunked(1800) 122 | 123 | if (msg.isNotEmpty()) { 124 | val firstMessage = logAndReturnMessage( 125 | guild, 126 | "**Info ::** ${reaction.name} used by ${moderator.username} on ${target.mention}\n" + 127 | "**Message:**```\n" + 128 | "${msg.first()}\n```" 129 | ) 130 | 131 | val rest = msg.takeLast(msg.size - 1).map { 132 | logAndReturnMessage(guild, "**Continued:**```\n$it\n```") 133 | } 134 | 135 | return listOf(firstMessage).plus(rest) 136 | } else if (message.attachments.isNotEmpty()) { 137 | return listOf( 138 | logAndReturnMessage( 139 | guild, 140 | "**Info ::** ${reaction.name} used by ${moderator.username} on ${target.mention}\n" + 141 | "**Message: (message was attachment, so only filename is logged)**```\n" + 142 | "${message.attachments.first().filename}\n```" 143 | ) 144 | ) 145 | } 146 | return emptyList() 147 | } 148 | 149 | suspend fun pointDecayApplied( 150 | guild: Guild, 151 | target: GuildMember, 152 | newPoints: Int, 153 | pointsDeducted: Int, 154 | weeksSinceLastInfraction: Int 155 | ) { 156 | val user = guild.kord.getUser(Snowflake(target.userId)) 157 | 158 | log( 159 | guild, 160 | "**Info ::** Infraction point decay for ${user?.descriptor()} " + 161 | "\nUser's points are now **$newPoints** after **$weeksSinceLastInfraction** infraction free weeks. " + 162 | "Previous points were ${newPoints + pointsDeducted}" 163 | ) 164 | logger.info { buildGuildLogMessage(guild, "Point decay applied to ${user?.idDescriptor()}. Points reduced from ${newPoints + pointsDeducted} to $newPoints for $weeksSinceLastInfraction weeks of decay") } 165 | } 166 | 167 | private suspend fun log(guild: Guild, message: String) { 168 | getLoggingChannel(guild)?.createMessage(message) 169 | } 170 | 171 | private suspend fun logAndReturnMessage(guild: Guild, message: String): Message? { 172 | return getLoggingChannel(guild)?.createMessage(message) 173 | } 174 | 175 | private suspend fun getLoggingChannel(guild: Guild): TextChannel? { 176 | val channelId = configuration[guild.id]?.loggingConfiguration?.loggingChannel ?: return null 177 | return guild.getChannelOf(channelId) 178 | } 179 | 180 | private fun buildGuildLogMessage(guild: Guild, message: String) = "${guild.name} (${guild.id}): $message" 181 | } 182 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/Collection.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database 2 | 3 | abstract class Collection(val name: String) -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/ConnectionService.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database 2 | 3 | import me.ddivad.judgebot.dataclasses.Configuration 4 | import me.jakejmattson.discordkt.annotations.Service 5 | import org.litote.kmongo.coroutine.CoroutineClient 6 | import org.litote.kmongo.coroutine.CoroutineDatabase 7 | import org.litote.kmongo.coroutine.coroutine 8 | import org.litote.kmongo.reactivestreams.KMongo 9 | 10 | @Service 11 | class ConnectionService(config: Configuration) { 12 | private val client: CoroutineClient = KMongo.createClient(config.dbConfiguration.address).coroutine 13 | val db: CoroutineDatabase = client.getDatabase(config.dbConfiguration.databaseName) 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/GuildOperations.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database 2 | 3 | import dev.kord.core.entity.Guild 4 | import dev.kord.core.entity.Member 5 | import dev.kord.core.entity.User 6 | import me.ddivad.judgebot.dataclasses.* 7 | import me.jakejmattson.discordkt.annotations.Service 8 | import org.litote.kmongo.eq 9 | 10 | @Service 11 | class GuildOperations(connection: ConnectionService) { 12 | companion object: Collection("Guilds") 13 | 14 | private val guildCollection = connection.db.getCollection(name) 15 | 16 | suspend fun setupGuild(guild: Guild): GuildInformation { 17 | val guildConfig = GuildInformation(guild.id.toString(), guild.name) 18 | this.guildCollection.insertOne(guildConfig) 19 | return guildConfig 20 | } 21 | 22 | suspend fun getRules(guild: Guild): List { 23 | val guildInfo = this.getGuild(guild) 24 | return guildInfo.rules.filter { it.number != 0 }.sortedBy { it.number } 25 | } 26 | 27 | suspend fun getRulesForInfractionPrompt(guild: Guild): List { 28 | val guildInfo = this.getGuild(guild) 29 | return guildInfo.rules.sortedBy { it.number } 30 | } 31 | 32 | suspend fun getRule(guild: Guild, ruleId: Int): Rule? { 33 | val guildInfo = this.getGuild(guild) 34 | return guildInfo.rules.find { it.number == ruleId } 35 | } 36 | 37 | suspend fun addRule(guild: Guild, rule: Rule) { 38 | val guildInfo = this.getGuild(guild) 39 | updateGuild(guildInfo.addRule(rule)) 40 | } 41 | 42 | suspend fun editRule(guild: Guild, oldRule: Rule, updatedRule: Rule) { 43 | val guildInfo = this.getGuild(guild) 44 | guildInfo.editRule(oldRule, updatedRule) 45 | updateGuild(guildInfo) 46 | } 47 | 48 | suspend fun archiveRule(guild: Guild, ruleNumber: Int) { 49 | val guildInfo = this.getGuild(guild) 50 | guildInfo.archiveRule(ruleNumber) 51 | updateGuild(guildInfo) 52 | } 53 | 54 | suspend fun addPunishment(guild: Guild, punishment: Punishment) { 55 | this.getGuild(guild).addPunishment(punishment).let { updateGuild(it) } 56 | } 57 | 58 | suspend fun removePunishment(guild: Guild, userId: String, type: InfractionType) { 59 | this.getGuild(guild).removePunishment(userId, type).let { updateGuild(it) } 60 | } 61 | 62 | suspend fun addBan(guild: Guild, ban: Ban): Ban { 63 | this.getGuild(guild).addBan(ban).let { updateGuild(it) } 64 | return ban 65 | } 66 | 67 | suspend fun editBanReason(guild: Guild, userId: String, reason: String) { 68 | val guildInfo = this.getGuild(guild) 69 | guildInfo.bans.find { it.userId == userId }?.reason = reason 70 | updateGuild(guildInfo) 71 | } 72 | 73 | suspend fun removeBan(guild: Guild, userId: String) { 74 | this.getGuild(guild).removeBan(userId).let { updateGuild(it) } 75 | } 76 | 77 | suspend fun checkBanExists(guild: Guild, userId: String): Boolean { 78 | return this.getGuild(guild).checkBanExits(userId) 79 | } 80 | 81 | suspend fun checkPunishmentExists(guild: Guild, member: Member, type: InfractionType): List { 82 | return this.getGuild(guild).getPunishmentByType(type, member.asUser().id.toString()) 83 | } 84 | 85 | suspend fun getPunishmentByType(guild: Guild, userId: String, type: InfractionType): List { 86 | return this.getGuild(guild).getPunishmentByType(type, userId) 87 | } 88 | 89 | suspend fun getPunishmentsForUser(guild: Guild, user: User): List { 90 | return this.getGuild(guild).getPunishmentsByUser(user.id.toString()) 91 | } 92 | 93 | suspend fun getBanOrNull(guild: Guild, userId: String): Ban? { 94 | return this.getGuild(guild).bans.find { it.userId == userId } 95 | } 96 | 97 | suspend fun getPunishmentsForGuild(guild: Guild, type: InfractionType): List { 98 | return this.getGuild(guild).punishments.filter { it.type == type } 99 | } 100 | 101 | suspend fun getActivePunishments(guild: Guild): List { 102 | return this.getGuild(guild).punishments 103 | } 104 | 105 | private suspend fun getGuild(guild: Guild): GuildInformation { 106 | return guildCollection.findOne(GuildInformation::guildId eq guild.id.toString()) 107 | ?: GuildInformation(guild.id.toString(), guild.name) 108 | } 109 | 110 | private suspend fun updateGuild(guildInformation: GuildInformation): GuildInformation { 111 | guildCollection.updateOne(GuildInformation::guildId eq guildInformation.guildId, guildInformation) 112 | return guildInformation 113 | } 114 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/JoinLeaveOperations.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database 2 | 3 | import dev.kord.core.entity.Member 4 | import me.ddivad.judgebot.dataclasses.JoinLeave 5 | import me.jakejmattson.discordkt.annotations.Service 6 | import org.litote.kmongo.and 7 | import org.litote.kmongo.eq 8 | import org.litote.kmongo.setValue 9 | import java.time.Instant 10 | 11 | @Service 12 | class JoinLeaveOperations(connection: ConnectionService) { 13 | companion object: Collection("JoinLeaves") 14 | 15 | private val joinLeaveCollection = connection.db.getCollection(name) 16 | 17 | suspend fun createJoinLeaveRecord(guildId: String, target: Member) { 18 | val joinLeave = JoinLeave(guildId, target.id.toString(), target.joinedAt.toEpochMilliseconds()) 19 | joinLeaveCollection.insertOne(joinLeave) 20 | } 21 | 22 | suspend fun addLeaveData(guildId: String, userId: String) { 23 | joinLeaveCollection.findOneAndUpdate( 24 | and( 25 | JoinLeave::guildId eq guildId, 26 | JoinLeave::userId eq userId, 27 | JoinLeave::leaveDate eq null 28 | ), 29 | setValue(JoinLeave::leaveDate, Instant.now().toEpochMilli()) 30 | ) 31 | } 32 | 33 | suspend fun getMemberJoinLeaveDataForGuild(guildId: String, userId: String): List { 34 | return joinLeaveCollection.find( 35 | and( 36 | JoinLeave::guildId eq guildId, 37 | JoinLeave::userId eq userId, 38 | ) 39 | ).toList() 40 | } 41 | 42 | suspend fun createJoinLeaveRecordIfNotRecorded(guildId: String, target: Member) { 43 | if (this.getMemberJoinLeaveDataForGuild(guildId, target.id.toString()).isNotEmpty()) { 44 | return 45 | } 46 | this.createJoinLeaveRecord(guildId, target) 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/MessageDeleteOperations.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database 2 | 3 | import dev.kord.core.entity.Member 4 | import me.ddivad.judgebot.dataclasses.MessageDelete 5 | import me.jakejmattson.discordkt.annotations.Service 6 | import org.litote.kmongo.and 7 | import org.litote.kmongo.eq 8 | 9 | @Service 10 | class MessageDeleteOperations(connection: ConnectionService) { 11 | companion object: Collection("MessageDelete") 12 | 13 | private val messageDeleteCollection = connection.db.getCollection(name) 14 | 15 | suspend fun createMessageDeleteRecord(guildId: String, target: Member, messageLink: String?) { 16 | val record = MessageDelete(target.id.toString(), guildId, messageLink) 17 | messageDeleteCollection.insertOne(record) 18 | } 19 | 20 | suspend fun getMessageDeletesForMember(guildId: String, userId: String): List { 21 | return messageDeleteCollection.find( 22 | and( 23 | MessageDelete::guildId eq guildId, 24 | MessageDelete::userId eq userId, 25 | ) 26 | ).toList() 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/MetaOperations.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database 2 | 3 | import me.ddivad.judgebot.dataclasses.Meta 4 | import me.jakejmattson.discordkt.annotations.Service 5 | 6 | @Service 7 | class MetaOperations(connection: ConnectionService) { 8 | companion object: Collection("Meta") 9 | private val metaCollection = connection.db.getCollection(name) 10 | 11 | suspend fun getCurrentVersion() = metaCollection.findOne() 12 | suspend fun save(meta: Meta) = metaCollection.save(meta) 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/MigrationService.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database 2 | 3 | import me.ddivad.judgebot.dataclasses.Meta 4 | import me.ddivad.judgebot.services.DatabaseService 5 | import me.ddivad.judgebot.services.database.migrations.v1 6 | import me.ddivad.judgebot.services.database.migrations.v2 7 | import me.jakejmattson.discordkt.annotations.Service 8 | import mu.KotlinLogging 9 | 10 | @Service 11 | class MigrationService(private val database: DatabaseService, private val connection: ConnectionService) { 12 | private val logger = KotlinLogging.logger { } 13 | 14 | suspend fun runMigrations() { 15 | var meta = database.meta.getCurrentVersion() 16 | 17 | if (meta == null) { 18 | meta = Meta(0) 19 | database.meta.save(meta) 20 | } 21 | 22 | var currentVersion = meta.version 23 | logger.info { "Current DB Version: v$currentVersion" } 24 | 25 | while (true) { 26 | val nextVersion = currentVersion + 1 27 | try { 28 | when (nextVersion) { 29 | 1 -> ::v1 30 | 2 -> ::v2 31 | else -> break 32 | }(connection.db) 33 | 34 | } catch (t: Throwable) { 35 | logger.error(t) { "Failed to migrate database to v$nextVersion" } 36 | throw t 37 | } 38 | currentVersion = nextVersion 39 | } 40 | if (currentVersion != meta.version) { 41 | meta = meta.copy(version = currentVersion) 42 | database.meta.save(meta) 43 | logger.info { "Finished database migrations." } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/UserOperations.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database 2 | 3 | import dev.kord.core.entity.Guild 4 | import dev.kord.core.entity.User 5 | import me.ddivad.judgebot.dataclasses.* 6 | import me.ddivad.judgebot.services.LoggingService 7 | import me.jakejmattson.discordkt.annotations.Service 8 | import org.litote.kmongo.eq 9 | 10 | @Service 11 | class UserOperations( 12 | connection: ConnectionService, 13 | private val configuration: Configuration, 14 | private val joinLeaveService: JoinLeaveOperations, 15 | private val loggingService: LoggingService 16 | ) { 17 | companion object: Collection("Users") 18 | 19 | private val userCollection = connection.db.getCollection(name) 20 | 21 | suspend fun getOrCreateUser(target: User, guild: Guild): GuildMember { 22 | val userRecord = userCollection.findOne(GuildMember::userId eq target.id.toString()) 23 | return if (userRecord != null) { 24 | userRecord.ensureGuildDetailsPresent(guild.id.toString()) 25 | userRecord.checkPointDecay(guild, configuration[guild.id]!!, loggingService) 26 | this.updateUser(userRecord) 27 | target.asMemberOrNull(guild.id)?.let { 28 | joinLeaveService.createJoinLeaveRecordIfNotRecorded(guild.id.toString(), it) 29 | } 30 | userRecord 31 | } else { 32 | val guildMember = GuildMember(target.id.toString()) 33 | guildMember.guilds.add(GuildMemberDetails(guild.id.toString())) 34 | userCollection.insertOne(guildMember) 35 | guildMember 36 | } 37 | } 38 | 39 | suspend fun addNote(guild: Guild, user: GuildMember, note: String, moderator: String): GuildMember { 40 | user.addNote(note, moderator, guild) 41 | return this.updateUser(user) 42 | } 43 | 44 | suspend fun editNote( 45 | guild: Guild, 46 | user: GuildMember, 47 | noteId: Int, 48 | newContent: String, 49 | moderator: String 50 | ): GuildMember { 51 | user.editNote(guild, noteId, newContent, moderator) 52 | return this.updateUser(user) 53 | } 54 | 55 | suspend fun deleteNote(guild: Guild, user: GuildMember, noteId: Int): GuildMember { 56 | user.deleteNote(noteId, guild) 57 | return this.updateUser(user) 58 | } 59 | 60 | suspend fun addInfo(guild: Guild, user: GuildMember, information: Info): GuildMember { 61 | user.addInfo(information, guild) 62 | return this.updateUser(user) 63 | } 64 | 65 | suspend fun removeInfo(guild: Guild, user: GuildMember, noteId: Int): GuildMember { 66 | user.removeInfo(noteId, guild) 67 | return this.updateUser(user) 68 | } 69 | 70 | suspend fun addLinkedAccount(guild: Guild, user: GuildMember, userId: String): GuildMember { 71 | user.addLinkedAccount(guild, userId) 72 | return this.updateUser(user) 73 | } 74 | 75 | suspend fun removeLinkedAccount(guild: Guild, user: GuildMember, userId: String): GuildMember { 76 | user.removeLinkedAccount(guild, userId) 77 | return this.updateUser(user) 78 | } 79 | 80 | suspend fun cleanseNotes(guild: Guild, user: GuildMember): GuildMember { 81 | user.cleanseNotes(guild) 82 | return this.updateUser(user) 83 | } 84 | 85 | suspend fun addInfraction(guild: Guild, user: GuildMember, infraction: Infraction): Infraction { 86 | user.addInfraction(infraction, guild) 87 | infraction.punishment = getPunishmentForPoints(guild, user) 88 | user.updatePointDecayDate(guild, infraction.punishment?.duration ?: 0) 89 | this.updateUser(user) 90 | return infraction 91 | } 92 | 93 | suspend fun addMessageDelete(guild: Guild, user: GuildMember, deleteReaction: Boolean): GuildMember { 94 | user.addMessageDeleted(guild, deleteReaction) 95 | return this.updateUser(user) 96 | } 97 | 98 | suspend fun cleanseInfractions(guild: Guild, user: GuildMember): GuildMember { 99 | user.cleanseInfractions(guild) 100 | return this.updateUser(user) 101 | } 102 | 103 | suspend fun removeInfraction(guild: Guild, user: GuildMember, infractionId: Int): GuildMember { 104 | user.deleteInfraction(guild, infractionId) 105 | return this.updateUser(user) 106 | } 107 | 108 | suspend fun incrementUserHistory(user: GuildMember, guild: Guild): GuildMember { 109 | user.incrementHistoryCount(guild.id.toString()) 110 | return this.updateUser(user) 111 | } 112 | 113 | suspend fun resetUserRecord(guild: Guild, user: GuildMember): GuildMember { 114 | user.reset(guild) 115 | return this.updateUser(user) 116 | } 117 | 118 | suspend fun updatePointDecayState(guild: Guild, user: GuildMember, freeze: Boolean): GuildMember { 119 | user.updatePointDecayState(guild, freeze) 120 | return this.updateUser(user) 121 | } 122 | 123 | suspend fun enableThinIceMode(guild: Guild, user: GuildMember): GuildMember { 124 | user.enableThinIce(guild) 125 | return this.updateUser(user) 126 | } 127 | 128 | suspend fun addBanRecord(guild: Guild, user: GuildMember, ban: Ban): GuildMember { 129 | user.addBan(guild, ban) 130 | return this.updateUser(user) 131 | } 132 | 133 | suspend fun addUnbanRecord(guild: Guild, user: GuildMember, thinIce: Boolean): GuildMember { 134 | user.unban(guild, thinIce, configuration[guild.id]!!.infractionConfiguration.warnUpgradeThreshold) 135 | return this.updateUser(user) 136 | } 137 | 138 | private suspend fun updateUser(user: GuildMember): GuildMember { 139 | userCollection.updateOne(GuildMember::userId eq user.userId, user) 140 | return user 141 | } 142 | 143 | private fun getPunishmentForPoints(guild: Guild, guildMember: GuildMember): PunishmentLevel { 144 | val punishmentLevels = configuration[guild.id]?.punishments 145 | return punishmentLevels!!.filter { 146 | it.points <= guildMember.getGuildInfo(guild.id.toString()).points 147 | }.maxByOrNull { it.points }!! 148 | } 149 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/migrations/v1.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database.migrations 2 | 3 | import me.ddivad.judgebot.services.database.GuildOperations 4 | import me.ddivad.judgebot.services.database.JoinLeaveOperations 5 | import me.ddivad.judgebot.services.database.MessageDeleteOperations 6 | import me.ddivad.judgebot.services.database.UserOperations 7 | import mu.KotlinLogging 8 | import org.litote.kmongo.coroutine.CoroutineDatabase 9 | 10 | suspend fun v1(db: CoroutineDatabase) { 11 | val logger = KotlinLogging.logger { } 12 | 13 | logger.info { "Running v1 DM Migration" } 14 | db.createCollection(GuildOperations.name) 15 | db.createCollection(JoinLeaveOperations.name) 16 | db.createCollection(UserOperations.name) 17 | db.createCollection(MessageDeleteOperations.name) 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/database/migrations/v2.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.database.migrations 2 | 3 | import com.mongodb.client.model.ReplaceOneModel 4 | import me.ddivad.judgebot.dataclasses.Ban 5 | import me.ddivad.judgebot.dataclasses.GuildInformation 6 | import me.ddivad.judgebot.dataclasses.GuildMember 7 | import me.ddivad.judgebot.services.database.GuildOperations 8 | import me.ddivad.judgebot.services.database.UserOperations 9 | import mu.KotlinLogging 10 | import org.litote.kmongo.coroutine.CoroutineDatabase 11 | import org.litote.kmongo.eq 12 | import org.litote.kmongo.replaceOne 13 | 14 | data class Result(val guildId: String, val bans: List) 15 | 16 | val logger = KotlinLogging.logger { } 17 | 18 | suspend fun v2(db: CoroutineDatabase) { 19 | logger.info { "Running v2 DB Migration" } 20 | val userCollection = db.getCollection(UserOperations.name) 21 | val guildCollection = db.getCollection(GuildOperations.name) 22 | 23 | logger.info{ "Updating ban records" } 24 | val guildBans = guildCollection.find().toList().map { Result(it.guildId, it.bans) } 25 | val banDocuments = mutableListOf>() 26 | guildCollection.find().consumeEach { guild -> 27 | guild.bans.forEach { 28 | it.thinIce = false 29 | it.unbanTime = null 30 | it.dateTime = null 31 | } 32 | banDocuments.add(replaceOne(GuildInformation::guildId eq guild.guildId, guild)) 33 | } 34 | if (banDocuments.isNotEmpty()) { 35 | guildCollection.bulkWrite(requests = banDocuments) 36 | } 37 | 38 | logger.info{ "Updating user records" } 39 | val userDocuments = mutableListOf>() 40 | userCollection.find().consumeEach { user -> 41 | guildBans.forEach { gb -> 42 | val userBan = gb.bans.find { it.userId == user.userId } 43 | if (userBan != null) { 44 | user.guilds.find { it.guildId == gb.guildId }?.bans?.add(userBan) 45 | } 46 | } 47 | user.guilds.forEach { guildDetails -> guildDetails.pointDecayFrozen = false } 48 | userDocuments.add(replaceOne(GuildMember::userId eq user.userId, user)) 49 | } 50 | if (userDocuments.isNotEmpty()) { 51 | userCollection.bulkWrite(requests = userDocuments) 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/infractions/BadPfpService.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.infractions 2 | 3 | import dev.kord.common.exception.RequestException 4 | import dev.kord.core.behavior.ban 5 | import dev.kord.core.entity.Guild 6 | import dev.kord.core.entity.Member 7 | import kotlinx.coroutines.GlobalScope 8 | import kotlinx.coroutines.Job 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.launch 11 | import me.ddivad.judgebot.embeds.createBadPfpEmbed 12 | import me.ddivad.judgebot.services.DatabaseService 13 | import me.ddivad.judgebot.services.LoggingService 14 | import me.jakejmattson.discordkt.Discord 15 | import me.jakejmattson.discordkt.annotations.Service 16 | import me.jakejmattson.discordkt.extensions.pfpUrl 17 | import me.jakejmattson.discordkt.extensions.sendPrivateMessage 18 | 19 | @Service 20 | class BadPfpService( 21 | private val discord: Discord, 22 | private val muteService: MuteService, 23 | private val loggingService: LoggingService 24 | ) { 25 | private val badPfpTracker = hashMapOf, Job>() 26 | private suspend fun toKey(member: Member): Pair = 27 | member.guild.id.toString() to member.asUser().id.toString() 28 | 29 | suspend fun applyBadPfp(target: Member, guild: Guild) { 30 | val minutesUntilBan = 30L 31 | val timeLimit = 1000 * 60 * minutesUntilBan 32 | try { 33 | target.sendPrivateMessage { 34 | createBadPfpEmbed(guild, target) 35 | } 36 | } catch (ex: RequestException) { 37 | loggingService.dmDisabled(guild, target.asUser()) 38 | } 39 | muteService.applyMuteAndSendReason(target, timeLimit, "Mute for BadPfp.") 40 | loggingService.badBfpApplied(guild, target) 41 | badPfpTracker[toKey((target))] = GlobalScope.launch { 42 | delay(timeLimit) 43 | if (target.pfpUrl == discord.kord.getUser(target.id)?.pfpUrl) { 44 | GlobalScope.launch { 45 | delay(1000) 46 | guild.ban(target.id) { 47 | reason = "BadPfp - Having a bad pfp and refusing to change it." 48 | deleteMessagesDays = 1 49 | } 50 | loggingService.badPfpBan(guild, target) 51 | } 52 | } else { 53 | target.asUser().sendPrivateMessage("Thanks for changing you avatar. You will not be banned.") 54 | } 55 | } 56 | } 57 | 58 | suspend fun hasActiveBapPfp(target: Member): Boolean { 59 | return badPfpTracker.containsKey(toKey(target)) 60 | } 61 | 62 | suspend fun cancelBadPfp(guild: Guild, target: Member) { 63 | val key = toKey(target) 64 | if (hasActiveBapPfp(target)) { 65 | badPfpTracker[key]?.cancel() 66 | badPfpTracker.remove(key) 67 | loggingService.badPfpCancelled(guild, target) 68 | muteService.removeMute(guild, target.asUser()) 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/infractions/BanService.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.infractions 2 | 3 | import dev.kord.core.behavior.ban 4 | import dev.kord.core.entity.Guild 5 | import dev.kord.core.entity.User 6 | import me.ddivad.judgebot.dataclasses.Ban 7 | import me.ddivad.judgebot.services.DatabaseService 8 | import me.ddivad.judgebot.services.LoggingService 9 | import me.jakejmattson.discordkt.annotations.Service 10 | 11 | @Service 12 | class BanService( 13 | private val databaseService: DatabaseService, 14 | private val loggingService: LoggingService, 15 | ) { 16 | suspend fun banUser(target: User, guild: Guild, ban: Ban, deleteDays: Int = 0) { 17 | val guildMember = databaseService.users.getOrCreateUser(target, guild) 18 | val banRecord = Ban(target.id.toString(), ban.moderator, ban.reason) 19 | guild.ban(target.id) { 20 | deleteMessagesDays = deleteDays 21 | reason = ban.reason 22 | } 23 | databaseService.guilds.addBan(guild, banRecord) 24 | databaseService.users.addBanRecord(guild, guildMember, banRecord) 25 | } 26 | 27 | suspend fun unbanUser(target: User, guild: Guild, thinIce: Boolean = false) { 28 | val guildMember = databaseService.users.getOrCreateUser(target, guild) 29 | guild.unban(target.id).let { 30 | databaseService.guilds.removeBan(guild, target.id.toString()) 31 | databaseService.users.addUnbanRecord(guild, guildMember, thinIce) 32 | loggingService.userUnbanned(guild, target) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/infractions/InfractionService.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.infractions 2 | 3 | import dev.kord.common.exception.RequestException 4 | import dev.kord.core.behavior.edit 5 | import dev.kord.core.entity.Guild 6 | import dev.kord.core.entity.Member 7 | import dev.kord.core.entity.Message 8 | import dev.kord.x.emoji.Emojis 9 | import dev.kord.x.emoji.toReaction 10 | import me.ddivad.judgebot.dataclasses.* 11 | import me.ddivad.judgebot.embeds.createInfractionEmbed 12 | import me.ddivad.judgebot.embeds.createMessageDeleteEmbed 13 | import me.ddivad.judgebot.services.DatabaseService 14 | import me.ddivad.judgebot.services.LoggingService 15 | import me.jakejmattson.discordkt.annotations.Service 16 | import me.jakejmattson.discordkt.extensions.jumpLink 17 | import me.jakejmattson.discordkt.extensions.sendPrivateMessage 18 | 19 | @Service 20 | class InfractionService( 21 | private val configuration: Configuration, 22 | private val databaseService: DatabaseService, 23 | private val loggingService: LoggingService, 24 | private val banService: BanService, 25 | private val muteService: MuteService 26 | ) { 27 | suspend fun infract(target: Member, guild: Guild, userRecord: GuildMember, infraction: Infraction): Infraction { 28 | var rule: Rule? = null 29 | if (infraction.ruleNumber != null) { 30 | rule = databaseService.guilds.getRule(guild, infraction.ruleNumber) 31 | } 32 | return databaseService.users.addInfraction(guild, userRecord, infraction).also { 33 | try { 34 | target.asUser().sendPrivateMessage { 35 | createInfractionEmbed(guild, configuration[guild.id]!!, target, userRecord, it, rule) 36 | } 37 | } catch (ex: RequestException) { 38 | loggingService.dmDisabled(guild, target.asUser()) 39 | } 40 | loggingService.infractionApplied(guild, target.asUser(), it) 41 | applyPunishment(guild, target, it) 42 | } 43 | } 44 | 45 | private suspend fun applyPunishment(guild: Guild, target: Member, infraction: Infraction) { 46 | when (infraction.punishment?.punishment) { 47 | PunishmentType.NONE -> return 48 | PunishmentType.MUTE -> muteService.applyInfractionMute(target, infraction.punishment?.duration!!) 49 | PunishmentType.BAN -> { 50 | val punishment = Ban(target.id.toString(), infraction.moderator, infraction.reason) 51 | banService.banUser(target, guild, punishment) 52 | } 53 | } 54 | } 55 | 56 | suspend fun badName(member: Member) { 57 | val badNames = mutableListOf( 58 | "Stephen", "Bob", "Joe", "Timmy", "Arnold", "Jeff", "Tim", "Doug" 59 | ) 60 | member.edit { nickname = badNames.random() } 61 | } 62 | 63 | suspend fun deleteMessage(guild: Guild, target: Member, message: Message, moderator: Member) { 64 | message.delete() 65 | databaseService.users.addMessageDelete( 66 | guild, 67 | databaseService.users.getOrCreateUser(target, guild), 68 | true 69 | ) 70 | val deleteLogMessage = 71 | loggingService.deleteReactionUsed(guild, moderator, target, Emojis.wastebasket.toReaction(), message) 72 | databaseService.messageDeletes.createMessageDeleteRecord( 73 | guild.id.toString(), 74 | target, 75 | deleteLogMessage.first()?.jumpLink() 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/services/infractions/MuteService.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.services.infractions 2 | 3 | import dev.kord.common.entity.Permission 4 | import dev.kord.common.entity.Permissions 5 | import dev.kord.common.exception.RequestException 6 | import dev.kord.core.behavior.edit 7 | import dev.kord.core.entity.Guild 8 | import dev.kord.core.entity.Member 9 | import dev.kord.core.entity.PermissionOverwrite 10 | import dev.kord.core.entity.User 11 | import dev.kord.core.supplier.EntitySupplyStrategy 12 | import kotlinx.coroutines.Job 13 | import kotlinx.coroutines.flow.toList 14 | import kotlinx.coroutines.runBlocking 15 | import kotlinx.datetime.toKotlinInstant 16 | import me.ddivad.judgebot.dataclasses.Configuration 17 | import me.ddivad.judgebot.dataclasses.InfractionType 18 | import me.ddivad.judgebot.dataclasses.Punishment 19 | import me.ddivad.judgebot.embeds.createMuteEmbed 20 | import me.ddivad.judgebot.embeds.createUnmuteEmbed 21 | import me.ddivad.judgebot.services.DatabaseService 22 | import me.ddivad.judgebot.services.LoggingService 23 | import me.ddivad.judgebot.util.applyRoleWithTimer 24 | import me.jakejmattson.discordkt.Discord 25 | import me.jakejmattson.discordkt.annotations.Service 26 | import me.jakejmattson.discordkt.extensions.sendPrivateMessage 27 | import me.jakejmattson.discordkt.extensions.toSnowflake 28 | import mu.KotlinLogging 29 | import java.time.Instant 30 | import kotlin.time.DurationUnit 31 | import kotlin.time.toDuration 32 | import kotlin.time.toJavaDuration 33 | 34 | typealias GuildID = String 35 | typealias UserId = String 36 | 37 | enum class MuteState { 38 | None, 39 | Tracked, 40 | Untracked, 41 | TimedOut 42 | } 43 | 44 | @Service 45 | class MuteService( 46 | val configuration: Configuration, 47 | private val discord: Discord, 48 | private val databaseService: DatabaseService, 49 | private val loggingService: LoggingService 50 | ) { 51 | private val logger = KotlinLogging.logger { } 52 | private val muteTimerMap = hashMapOf, Job>() 53 | private suspend fun getMutedRole(guild: Guild) = guild.getRole(configuration[guild.id]?.mutedRole!!) 54 | private fun toKey(user: User, guild: Guild) = user.id.toString() to guild.id.toString() 55 | suspend fun initGuilds() { 56 | configuration.guildConfigurations.forEach { config -> 57 | runBlocking { 58 | try { 59 | val guild = config.key.let { discord.kord.getGuild(it) } ?: return@runBlocking 60 | initialiseMuteTimers(guild) 61 | setupMutedRole(guild) 62 | } catch (ex: Exception) { 63 | println(ex.message) 64 | } 65 | } 66 | } 67 | } 68 | 69 | suspend fun applyInfractionMute(member: Member, time: Long) { 70 | applyMute(member, time) 71 | } 72 | 73 | suspend fun applyMuteAndSendReason(member: Member, time: Long, reason: String) { 74 | val guild = member.guild.asGuild() 75 | val user = member.asUser() 76 | applyMute(member, time) 77 | try { 78 | member.sendPrivateMessage { 79 | createMuteEmbed(guild, member, reason, time) 80 | } 81 | } catch (ex: RequestException) { 82 | loggingService.dmDisabled(guild, user) 83 | } 84 | } 85 | 86 | private suspend fun applyMute(member: Member, time: Long) { 87 | val guild = member.guild.asGuild() 88 | val user = member.asUser() 89 | val clearTime = Instant.now().plus(time.toDuration(DurationUnit.MILLISECONDS).toJavaDuration()).toEpochMilli() 90 | val punishment = Punishment(user.id.toString(), InfractionType.Mute, clearTime) 91 | val muteRole = getMutedRole(guild) 92 | val timeoutDuration = Instant.ofEpochMilli(Instant.now().toEpochMilli() + time - 2000) 93 | val key = toKey(user, guild) 94 | if (key in muteTimerMap) { 95 | muteTimerMap[key]?.cancel() 96 | muteTimerMap.remove(key) 97 | databaseService.guilds.removePunishment(guild, member.asUser().id.toString(), InfractionType.Mute) 98 | loggingService.muteOverwritten(guild, member) 99 | } 100 | databaseService.guilds.addPunishment(guild.asGuild(), punishment) 101 | member.edit { communicationDisabledUntil = timeoutDuration.toKotlinInstant() } 102 | muteTimerMap[key] = applyRoleWithTimer(member, muteRole, time) { 103 | removeMute(guild, user) 104 | }.also { 105 | loggingService.roleApplied(guild, member.asUser(), muteRole) 106 | } 107 | } 108 | 109 | suspend fun gag(guild: Guild, target: Member, moderator: User) { 110 | val muteDuration = configuration[guild.id]?.infractionConfiguration?.gagDuration ?: return 111 | loggingService.gagApplied(guild, target, moderator) 112 | this.applyMuteAndSendReason(target, muteDuration, "You've been muted temporarily by staff.") 113 | } 114 | 115 | fun removeMute(guild: Guild, user: User) { 116 | runBlocking { 117 | val muteRole = getMutedRole(guild) 118 | val key = toKey(user, guild) 119 | guild.getMemberOrNull(user.id)?.let { 120 | it.removeRole(muteRole.id) 121 | it.edit { communicationDisabledUntil = Instant.now().toKotlinInstant() } 122 | try { 123 | it.sendPrivateMessage { 124 | createUnmuteEmbed(guild, user) 125 | } 126 | } catch (ex: RequestException) { 127 | loggingService.dmDisabled(guild, user) 128 | } 129 | loggingService.roleRemoved(guild, user, muteRole) 130 | if (checkMuteState(guild, it) == MuteState.Untracked) return@runBlocking 131 | } 132 | databaseService.guilds.removePunishment(guild, user.id.toString(), InfractionType.Mute) 133 | muteTimerMap[key]?.cancel() 134 | muteTimerMap.remove(key) 135 | } 136 | } 137 | 138 | private suspend fun initialiseMuteTimers(guild: Guild) { 139 | runBlocking { 140 | val punishments = databaseService.guilds.getPunishmentsForGuild(guild, InfractionType.Mute) 141 | logger.info { "${guild.name} (${guild.id}): Existing Punishments :: ${punishments.size} existing punishments found for ${guild.name}" } 142 | punishments.forEach { 143 | if (it.clearTime != null) { 144 | logger.info { "${guild.name} (${guild.id}): Adding Existing Timer :: UserId: ${it.userId}, GuildId: ${guild.id.value}, PunishmentId: ${it.id}" } 145 | val difference = it.clearTime - Instant.now().toEpochMilli() 146 | val member = guild.getMemberOrNull(it.userId.toSnowflake()) ?: return@forEach 147 | val user = member.asUser() 148 | val key = toKey(user, guild) 149 | muteTimerMap[key] = applyRoleWithTimer(member, getMutedRole(guild), difference) { 150 | removeMute(guild, user) 151 | } 152 | } 153 | } 154 | } 155 | loggingService.initialiseMutes(guild, getMutedRole(guild)) 156 | } 157 | 158 | suspend fun handleRejoinMute(guild: Guild, member: Member) { 159 | val mute = databaseService.guilds.checkPunishmentExists(guild, member, InfractionType.Mute).first() 160 | if (mute.clearTime != null) { 161 | val difference = mute.clearTime - Instant.now().toEpochMilli() 162 | val user = member.asUser() 163 | val key = toKey(user, guild) 164 | muteTimerMap[key] = applyRoleWithTimer(member, getMutedRole(guild), difference) { 165 | removeMute(guild, user) 166 | } 167 | } 168 | } 169 | 170 | suspend fun checkMuteState(guild: Guild, member: Member): MuteState { 171 | return if (databaseService.guilds.checkPunishmentExists(guild, member, InfractionType.Mute) 172 | .isNotEmpty() 173 | ) MuteState.Tracked 174 | else if (member.roles.toList().contains(getMutedRole(member.getGuild()))) MuteState.Untracked 175 | else if (member.communicationDisabledUntil != null && member.communicationDisabledUntil!! > Instant.now() 176 | .toKotlinInstant() 177 | ) { 178 | MuteState.TimedOut 179 | } else MuteState.None 180 | } 181 | 182 | suspend fun setupMutedRole(guild: Guild) { 183 | val mutedRole = guild.getRole(configuration[guild.id]!!.mutedRole) 184 | guild.withStrategy(EntitySupplyStrategy.cachingRest).channels.toList().forEach { 185 | val deniedPermissions = it.getPermissionOverwritesForRole(mutedRole.id)?.denied ?: Permissions() 186 | if (deniedPermissions.values.any { permission -> 187 | permission in setOf( 188 | Permission.SendMessages, 189 | Permission.AddReactions, 190 | Permission.CreatePublicThreads, 191 | Permission.CreatePrivateThreads, 192 | Permission.SendMessagesInThreads 193 | ) 194 | }) { 195 | try { 196 | it.addOverwrite( 197 | PermissionOverwrite.forRole( 198 | mutedRole.id, 199 | denied = deniedPermissions.plus(Permission.SendMessages).plus(Permission.AddReactions) 200 | .plus(Permission.CreatePrivateThreads).plus(Permission.CreatePrivateThreads) 201 | .plus(Permission.SendMessagesInThreads) 202 | ), 203 | "Judgebot Overwrite" 204 | ) 205 | } catch (ex: RequestException) { 206 | logger.warn { "${guild.name} (${guild.id}): No permssions to add overwrite to ${it.id.value} - ${it.name}" } 207 | } 208 | } 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/util/MessageUtils.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.util 2 | 3 | import dev.kord.common.entity.ChannelType 4 | import dev.kord.core.entity.Message 5 | import dev.kord.core.entity.User 6 | import dev.kord.core.entity.channel.MessageChannel 7 | import dev.kord.core.entity.channel.thread.ThreadChannel 8 | import me.jakejmattson.discordkt.extensions.jumpLink 9 | 10 | suspend fun createFlagMessage(user: User, message: Message, channel: MessageChannel): String { 11 | val isThread = channel.type in setOf(ChannelType.PublicGuildThread, ChannelType.PrivateThread) 12 | 13 | return "**Message Flagged**" + 14 | "\n**User**: ${user.mention}" + 15 | (if (isThread) 16 | "\n**Thread**: ${channel.mention} (${(channel as? ThreadChannel)?.parent?.mention})" 17 | else 18 | "\n**Channel**: ${channel.mention}") + 19 | "\n**Author:** ${message.author?.mention}" + 20 | "\n**Message:** ${message.jumpLink()}" 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/util/NumberUtils.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.util 2 | 3 | fun timeToString(milliseconds: Long): String { 4 | val seconds = (milliseconds / 1000) % 60 5 | val minutes = (milliseconds / (1000 * 60)) % 60 6 | val hours = (milliseconds / (1000 * 60 * 60)) % 24 7 | val days = (milliseconds / (1000 * 60 * 60 * 24)) 8 | val dayString = if (days > 0) "$days day(s) " else "" 9 | val hourString = if (hours > 0) "$hours hour(s) " else "" 10 | val minuteString = if (minutes > 0) "$minutes minute(s) " else "" 11 | val secondString = if (seconds > 0) "$seconds second(s)" else "" 12 | return ("$dayString$hourString$minuteString$secondString") 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/me/ddivad/judgebot/util/TimerUtils.kt: -------------------------------------------------------------------------------- 1 | package me.ddivad.judgebot.util 2 | 3 | import dev.kord.core.entity.Member 4 | import dev.kord.core.entity.Role 5 | import kotlinx.coroutines.GlobalScope 6 | import kotlinx.coroutines.Job 7 | import kotlinx.coroutines.delay 8 | import kotlinx.coroutines.launch 9 | 10 | suspend fun applyRoleWithTimer(member: Member, role: Role, millis: Long, fn: (Member) -> Unit): Job { 11 | member.addRole(role.id) 12 | return GlobalScope.launch { 13 | delay(millis) 14 | fn(member) 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/resources/bot.properties: -------------------------------------------------------------------------------- 1 | #Sat Feb 04 22:24:41 GMT 2023 2 | description=A bot for managing discord infractions in an intelligent and user-friendly way. 3 | name=Judgebot 4 | url=https\://github.com/the-programmers-hangout/JudgeBot/ 5 | version=2.9.0 6 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %boldGreen(%d{yyyy-MM-dd}) %boldYellow(%d{HH:mm:ss}) %gray(|) %highlight(%5level) %gray(|) %boldMagenta(%40.40logger{40}) %gray(|) %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------