diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d521601..ae6fd1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,36 +7,33 @@ on: - develop pull_request: + branches-ignore: + - root jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.17 + java-version: 17 + distribution: temurin - - name: Gradle (Build) + - name: Build uses: gradle/gradle-build-action@v2 with: arguments: build - - name: Upload artifacts (Main JAR) - uses: actions/upload-artifact@v2 - - with: - name: Main JAR - path: build/libs/*-all.jar - - name: Upload artifacts (JARs) - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: JARs path: build/libs/*.jar + if-no-files-found: warn diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 409f750..8b30e86 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -10,30 +10,25 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.17 + java-version: 17 + distribution: temurin - - name: Gradle (Build) + - name: Build uses: gradle/gradle-build-action@v2 with: arguments: build - - name: Upload artifacts (Main JAR) - uses: actions/upload-artifact@v2 - - with: - name: Main JAR - path: build/libs/*-all.jar - - name: Upload artifacts (JARs) - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: JARs path: build/libs/*.jar + if-no-files-found: warn diff --git a/.github/workflows/root.yml b/.github/workflows/root.yml index da08301..6ba0aae 100644 --- a/.github/workflows/root.yml +++ b/.github/workflows/root.yml @@ -10,31 +10,49 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 + with: + fetch-tags: true - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.17 + java-version: 17 + distribution: temurin - - name: Gradle (Build) + - name: Build uses: gradle/gradle-build-action@v2 with: arguments: build dependency-graph: generate-and-submit - - name: Upload artifacts (Main JAR) - uses: actions/upload-artifact@v2 + - name: Upload artifacts + uses: actions/upload-artifact@v3 with: - name: Main JAR - path: build/libs/*-all.jar + name: JARs + path: build/libs/*.jar + if-no-files-found: warn - - name: Upload artifacts (JARs) - uses: actions/upload-artifact@v2 + - name: Validate version + id: version + run: | + VERSION=$(cat .version) + echo "version=${VERSION}" >> $GITHUB_OUTPUT + if git show-ref --tags --verify --quiet "refs/tags/${VERSION}"; then + echo "Version ${VERSION} was already released" + exit 0 + fi + + - name: Release artifacts + uses: marvinpinto/action-automatic-releases@v1 with: - name: JARs - path: build/libs/*.jar + repo_token: ${{ secrets.GITHUB_TOKEN }} + prerelease: false + automatic_release_tag: ${{ steps.version.outputs.version }} + files: | + build/libs/*.jar + LICENSE.md diff --git a/.gitignore b/.gitignore index e798797..cfd0ada 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ hs_err_pid* # Generated files .idea/**/contentModel.xml +.idea/.name # Sensitive or high-churn files .idea/**/dataSources/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..10b773b --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 3657fb2..b838806 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/.version b/.version new file mode 100644 index 0000000..388bb06 --- /dev/null +++ b/.version @@ -0,0 +1 @@ +0.1.0-alpha diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..bfa8437 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,356 @@ +Mozilla Public License Version 2.0 +================================== + +### 1. Definitions + +**1.1. “Contributor”** +means each individual or legal entity that creates, contributes to +the creation of, or owns Covered Software. + +**1.2. “Contributor Version”** +means the combination of the Contributions of others (if any) used +by a Contributor and that particular Contributor's Contribution. + +**1.3. “Contribution”** +means Covered Software of a particular Contributor. + +**1.4. “Covered Software”** +means Source Code Form to which the initial Contributor has attached +the notice in Exhibit A, the Executable Form of such Source Code +Form, and Modifications of such Source Code Form, in each case +including portions thereof. + +**1.5. “Incompatible With Secondary Licenses”** +means + +* **(a)** that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or +* **(b)** that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +**1.6. “Executable Form”** +means any form of the work other than Source Code Form. + +**1.7. “Larger Work”** +means a work that combines Covered Software with other material, in +a separate file or files, that is not Covered Software. + +**1.8. “License”** +means this document. + +**1.9. “Licensable”** +means having the right to grant, to the maximum extent possible, +whether at the time of the initial grant or subsequently, any and +all of the rights conveyed by this License. + +**1.10. “Modifications”** +means any of the following: + +* **(a)** any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or +* **(b)** any new file in Source Code Form that contains any Covered + Software. + +**1.11. “Patent Claims” of a Contributor** +means any patent claim(s), including without limitation, method, +process, and apparatus claims, in any patent Licensable by such +Contributor that would be infringed, but for the grant of the +License, by the making, using, selling, offering for sale, having +made, import, or transfer of either its Contributions or its +Contributor Version. + +**1.12. “Secondary License”** +means either the GNU General Public License, Version 2.0, the GNU +Lesser General Public License, Version 2.1, the GNU Affero General +Public License, Version 3.0, or any later versions of those +licenses. + +**1.13. “Source Code Form”** +means the form of the work preferred for making modifications. + +**1.14. “You” (or “Your”)** +means an individual or a legal entity exercising rights under this +License. For legal entities, “You” includes any entity that +controls, is controlled by, or is under common control with You. For +purposes of this definition, “control” means **(a)** the power, direct +or indirect, to cause the direction or management of such entity, +whether by contract or otherwise, or **(b)** ownership of more than +fifty percent (50%) of the outstanding shares or beneficial +ownership of such entity. + + +### 2. License Grants and Conditions + +#### 2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +* **(a)** under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and +* **(b)** under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +#### 2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +#### 2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +* **(a)** for any code that a Contributor has removed from Covered Software; + or +* **(b)** for infringements caused by: **(i)** Your and any other third party's + modifications of Covered Software, or **(ii)** the combination of its + Contributions with other software (except as part of its Contributor + Version); or +* **(c)** under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +#### 2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +#### 2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +#### 2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +#### 2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + + +### 3. Responsibilities + +#### 3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +#### 3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +* **(a)** such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +* **(b)** You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +#### 3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +#### 3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +#### 3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + + +### 4. Inability to Comply Due to Statute or Regulation + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: **(a)** comply with +the terms of this License to the maximum extent possible; and **(b)** +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + + +### 5. Termination + +**5.1.** The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated **(a)** provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and **(b)** on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +**5.2.** If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +**5.3.** In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + + +### 6. Disclaimer of Warranty + +> Covered Software is provided under this License on an “as is” +> basis, without warranty of any kind, either expressed, implied, or +> statutory, including, without limitation, warranties that the +> Covered Software is free of defects, merchantable, fit for a +> particular purpose or non-infringing. The entire risk as to the +> quality and performance of the Covered Software is with You. +> Should any Covered Software prove defective in any respect, You +> (not any Contributor) assume the cost of any necessary servicing, +> repair, or correction. This disclaimer of warranty constitutes an +> essential part of this License. No use of any Covered Software is +> authorized under this License except under this disclaimer. + +### 7. Limitation of Liability + +> Under no circumstances and under no legal theory, whether tort +> (including negligence), contract, or otherwise, shall any +> Contributor, or anyone who distributes Covered Software as +> permitted above, be liable to You for any direct, indirect, +> special, incidental, or consequential damages of any character +> including, without limitation, damages for lost profits, loss of +> goodwill, work stoppage, computer failure or malfunction, or any +> and all other commercial damages or losses, even if such party +> shall have been informed of the possibility of such damages. This +> limitation of liability shall not apply to liability for death or +> personal injury resulting from such party's negligence to the +> extent applicable law prohibits such limitation. Some +> jurisdictions do not allow the exclusion or limitation of +> incidental or consequential damages, so this exclusion and +> limitation may not apply to You. + + +### 8. Litigation + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + + +### 9. Miscellaneous + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + + +### 10. Versions of the License + +#### 10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +#### 10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +#### 10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +#### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +## Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +## Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + diff --git a/README.md b/README.md index 107d1ca..2667a3b 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,17 @@ -# KordEx Bot Template - -This repository contains a basic KordEx example bot for you to use as a template for your own KordEx bots. This -includes the following: - -* A basic extension that allows you to slap other people, using both message commands and slash commands. -* A basic bot configuration that enables slash commands and shows you how to conditionally provide a different - message command prefix for different guilds. -* A Gradle Kotlin build script that uses the Kotlin Discord public maven repo, Detekt for linting (with a - fairly strict configuration) and a Git commit hook plugin that runs Detekt when you make a commit - this uses Gradle - 7's new version catalog feature, for easy configuration of dependencies. -* GitHub CI scripts that build the bot and publish its artefacts. -* A reasonable `.gitignore` file, including one in the `.idea` folder that ignores files that you shouldn't commit - - if you're using IDEA yourself, you should install the Ignore plugin to handle changes to this for you. -* A Groovy-based Logback config, so you've reasonable logging out of the box. - -**Note:** This template includes a `.editorconfig` file that defaults to using tabs for indentation in almost all file -types. This is because tabs are more accessible for the blind, or those with impaired vision. We won't accept -feedback or PRs targeting this approach. - -## Potential Changes - -* The `.yml` files in `.github/` are used to configure GitHub apps. If you're not using them, you can remove them. -* The provided `LICENSE` file contains The Unlicense, which makes this repository public domain. You will probably want - to change this - we suggest looking at [Choose a License](https://choosealicense.com/) if you're not sure where to start. -* In the `build.gradle.kts`: - * Set the `group` and `version` properties as appropriate. - * If you're not using this to test KordEx builds, you can remove the `mavenLocal()` from the `repositories` block. - * In the `application` and `tasks.jar` blocks, update the main class path/name as appropriate. - * To target a newer/older Java version, change the options in the `KotlinCompile` configuration and `java` blocks -* In the `settings.gradle.kts`, update the name of the root project as appropriate. -* The bundled Detekt config is pretty strict - you can check over `detekt.yml` if you want to change it, but you need to - follow the TODOs in that file regardless. -* The Logback configuration is in `src/main/resources/logback.groovy`. If the logging setup doesn't suit, you can change - it there. - -## Bundled Bot - -* `App.kt` includes a basic bot, which uses environment variables (or variables in a `.env` file) for the testing guild - ID (`TEST_SERVER`) and the bot's token (`TOKEN`). You can specify these either directly as environment variables, or - as `KEY=value` pairs in a file named `.env`. Some example code is also included that shows one potential way of - providing different command prefixes for different servers. -* `TestExtension.kt` includes an example extension that creates a `slap` command - this command works as both a - message command and slash command, and allows you to slap other users with whatever you wish, defaulting to a - `large, smelly trout`. - -To test the bot, we recommend using a `.env` file that looks like the following: - -```dotenv -TOKEN=abc... -TEST_SERVER=123... +# Super Trouper + +## Running + +Create a `.env` file before running the bot. + +```properties +TOKEN= +AUTOMATIC_CHANNEL_CREATION_MEMBER_LIMIT=30 +IS_DEV_ENV=true +DONATE_URL= +LICENSE=Mozilla Public License 2.0 +LICENSE_URL=https://www.mozilla.org/en-US/MPL/2.0/ +OFFICIAL_SERVER=1130171551636004864 +OFFICIAL_SERVER_URL=https://discord.gg/xJu6MH2KUc +REPO_URL=https://github.com/LaylaMeower/SuperTrouper ``` - -Create this file, fill it out, and run the `run` gradle task for testing in development. diff --git a/assets/SuperTrouper.pxo b/assets/SuperTrouper.pxo new file mode 100644 index 0000000..ede04f9 Binary files /dev/null and b/assets/SuperTrouper.pxo differ diff --git a/assets/SuperTrouper_1024x.png b/assets/SuperTrouper_1024x.png new file mode 100644 index 0000000..d505603 Binary files /dev/null and b/assets/SuperTrouper_1024x.png differ diff --git a/assets/SuperTrouper_128x.png b/assets/SuperTrouper_128x.png new file mode 100644 index 0000000..3be9d7e Binary files /dev/null and b/assets/SuperTrouper_128x.png differ diff --git a/assets/SuperTrouper_16x.png b/assets/SuperTrouper_16x.png new file mode 100644 index 0000000..2785dcb Binary files /dev/null and b/assets/SuperTrouper_16x.png differ diff --git a/assets/TrouperDev.pxo b/assets/TrouperDev.pxo new file mode 100644 index 0000000..3954696 Binary files /dev/null and b/assets/TrouperDev.pxo differ diff --git a/assets/TrouperDev_1024x.png b/assets/TrouperDev_1024x.png new file mode 100644 index 0000000..f57016b Binary files /dev/null and b/assets/TrouperDev_1024x.png differ diff --git a/assets/TrouperDev_128x.png b/assets/TrouperDev_128x.png new file mode 100644 index 0000000..cd7f5c4 Binary files /dev/null and b/assets/TrouperDev_128x.png differ diff --git a/assets/TrouperDev_16x.png b/assets/TrouperDev_16x.png new file mode 100644 index 0000000..fd9279f Binary files /dev/null and b/assets/TrouperDev_16x.png differ diff --git a/build.gradle.kts b/build.gradle.kts index 98955df..cc022c0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.util.removeSuffixIfPresent plugins { application @@ -7,11 +8,10 @@ plugins { kotlin("plugin.serialization") id("com.github.johnrengelman.shadow") - id("io.gitlab.arturbosch.detekt") } group = "quest.laxla" -version = "0.0.1" +version = file(".version").readText().removeSuffixIfPresent("\n") repositories { google() @@ -26,7 +26,6 @@ repositories { } } -val detekt: String by project val kordex: String by project val serialization: String by project val logback: String by project @@ -35,24 +34,33 @@ val klogging: String by project dependencies { implementation(kotlin("stdlib")) - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:$serialization") + implementation(kotlin("reflect")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$serialization") + implementation("com.kotlindiscord.kord.extensions:kord-extensions:$kordex") implementation("io.github.oshai:kotlin-logging:$klogging") runtimeOnly("org.slf4j:slf4j-api:$slf4j") runtimeOnly("ch.qos.logback:logback-classic:$logback") +} + +val generatedResources = layout.buildDirectory.dir("generated/resources") + +tasks.processResources { + from(generatedResources) - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detekt") + doFirst { + generatedResources.orNull?.run { + asFile.mkdirs() + file(".version").asFile.writeText(version.toString()) + } + } } application { - mainClass = "quest.laxla.supertrouper.AppKt" + mainClass = "quest.laxla.trouper.AppKt" } -val jvm: String by project - -tasks.withType { kotlinOptions.jvmTarget = jvm } - tasks.jar { manifest { attributes( @@ -61,14 +69,12 @@ tasks.jar { } } +val jvm: String by project + +tasks.withType { kotlinOptions.jvmTarget = jvm } + java { val java = JavaVersion.toVersion(jvm) sourceCompatibility = java targetCompatibility = java } - -detekt { - buildUponDefaultConfig = true - - config.from(rootProject.files("detekt.yml")) -} diff --git a/detekt.yml b/detekt.yml deleted file mode 100644 index e3ddb48..0000000 --- a/detekt.yml +++ /dev/null @@ -1,679 +0,0 @@ -# TODO: Update `rootPackage` in naming -> InvalidPackageDeclaration - -build: - maxIssues: 0 - excludeCorrectable: false - weights: - # complexity: 2 - # LongParameterList: 1 - # style: 1 - # comments: 1 - -config: - validation: true - # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' - excludes: '' - -processors: - active: true - exclude: - - 'DetektProgressListener' - - 'FunctionCountProcessor' - - 'PropertyCountProcessor' - - 'ClassCountProcessor' - - 'PackageCountProcessor' - - 'KtFileCountProcessor' - -console-reports: - active: true - exclude: - - 'ProjectStatisticsReport' - - 'NotificationReport' - - 'FileBasedFindingsReport' - -output-reports: - active: true - exclude: - # - 'HtmlOutputReport' - - 'TxtOutputReport' - # - 'XmlOutputReport' - -comments: - active: true - - AbsentOrWrongFileLicense: - active: false - licenseTemplateFile: 'license.template' - CommentOverPrivateFunction: - active: false - CommentOverPrivateProperty: - active: false - EndOfSentenceFormat: - active: true - endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - UndocumentedPublicClass: - active: false - searchInNestedClass: true - searchInInnerClass: true - searchInInnerObject: true - searchInInnerInterface: true - UndocumentedPublicFunction: - active: false - UndocumentedPublicProperty: - active: false - -complexity: - active: true - ComplexCondition: - active: true - threshold: 10 - ComplexInterface: - active: false - threshold: 10 - includeStaticDeclarations: false - includePrivateDeclarations: false - ComplexMethod: - active: false - threshold: 15 - ignoreSingleWhenExpression: false - ignoreSimpleWhenEntries: false - ignoreNestingFunctions: false - nestingFunctions: [ run, let, apply, with, also, use, forEach, isNotNull, ifNull ] - LabeledExpression: - active: false - ignoredLabels: [ ] - LargeClass: - active: false - threshold: 600 - LongMethod: - active: false - threshold: 60 - LongParameterList: - active: false - functionThreshold: 6 - constructorThreshold: 7 - ignoreDefaultParameters: false - ignoreDataClasses: true - ignoreAnnotated: [ ] - MethodOverloading: - active: false - threshold: 6 - NestedBlockDepth: - active: false - threshold: 4 - ReplaceSafeCallChainWithRun: - active: true - StringLiteralDuplication: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - threshold: 3 - ignoreAnnotation: true - excludeStringsWithLessThan5Characters: true - ignoreStringsRegex: '$^' - TooManyFunctions: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - thresholdInFiles: 11 - thresholdInClasses: 11 - thresholdInInterfaces: 11 - thresholdInObjects: 11 - thresholdInEnums: 11 - ignoreDeprecated: false - ignorePrivate: false - ignoreOverridden: false - -coroutines: - active: true - GlobalCoroutineUsage: - active: true - RedundantSuspendModifier: - active: true - SuspendFunWithFlowReturnType: - active: true - -empty-blocks: - active: true - EmptyCatchBlock: - active: true - allowedExceptionNameRegex: '^(_|(ignore|expected).*)' - EmptyClassBlock: - active: true - EmptyDefaultConstructor: - active: true - EmptyDoWhileBlock: - active: true - EmptyElseBlock: - active: true - EmptyFinallyBlock: - active: true - EmptyForBlock: - active: true - EmptyFunctionBlock: - active: true - ignoreOverridden: false - EmptyIfBlock: - active: true - EmptyInitBlock: - active: true - EmptyKtFile: - active: true - EmptySecondaryConstructor: - active: true - EmptyTryBlock: - active: true - EmptyWhenBlock: - active: true - EmptyWhileBlock: - active: true - -exceptions: - active: true - ExceptionRaisedInUnexpectedLocation: - active: true - methodNames: [ toString, hashCode, equals, finalize ] - InstanceOfCheckForException: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - NotImplementedDeclaration: - active: false - PrintStackTrace: - active: true - RethrowCaughtException: - active: true - ReturnFromFinally: - active: true - ignoreLabeled: false - SwallowedException: - active: false - ignoredExceptionTypes: - - InterruptedException - - NumberFormatException - - ParseException - - MalformedURLException - allowedExceptionNameRegex: '^(_|(ignore|expected).*)' - ThrowingExceptionFromFinally: - active: true - ThrowingExceptionInMain: - active: true - ThrowingExceptionsWithoutMessageOrCause: - active: true - exceptions: - - IllegalArgumentException - - IllegalStateException - - IOException - ThrowingNewInstanceOfSameException: - active: true - TooGenericExceptionCaught: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - exceptionNames: - - ArrayIndexOutOfBoundsException - - Error - - Exception - - IllegalMonitorStateException - - NullPointerException - - IndexOutOfBoundsException - - RuntimeException - - Throwable - allowedExceptionNameRegex: '^(_|(ignore|expected).*)' - TooGenericExceptionThrown: - active: true - exceptionNames: - - Error - - Exception - - Throwable - - RuntimeException - -formatting: - active: true - android: false - autoCorrect: true - AnnotationOnSeparateLine: - active: true - autoCorrect: true - AnnotationSpacing: - active: true - autoCorrect: true - ArgumentListWrapping: - active: false # It's wrong! - autoCorrect: true - ChainWrapping: - active: true - autoCorrect: true - CommentSpacing: - active: true - autoCorrect: true - EnumEntryNameCase: - active: true - autoCorrect: true - Filename: - active: true - FinalNewline: - active: true - autoCorrect: true - insertFinalNewLine: true - ImportOrdering: - active: true - autoCorrect: true - layout: "*,java.**,javax.**,kotlin.**,^" - Indentation: - active: false - autoCorrect: false - indentSize: 4 - continuationIndentSize: 4 - MaximumLineLength: - active: true - maxLineLength: 120 - ModifierOrdering: - active: true - autoCorrect: true - MultiLineIfElse: - active: true - autoCorrect: true - NoBlankLineBeforeRbrace: - active: true - autoCorrect: true - NoConsecutiveBlankLines: - active: true - autoCorrect: true - NoEmptyClassBody: - active: true - autoCorrect: true - NoEmptyFirstLineInMethodBlock: - active: true - autoCorrect: true - NoLineBreakAfterElse: - active: true - autoCorrect: true - NoLineBreakBeforeAssignment: - active: true - autoCorrect: true - NoMultipleSpaces: - active: false - autoCorrect: false - NoSemicolons: - active: true - autoCorrect: true - NoTrailingSpaces: - active: true - autoCorrect: true - NoUnitReturn: - active: true - autoCorrect: true - NoUnusedImports: - active: true - autoCorrect: true - NoWildcardImports: - active: false - PackageName: - active: true - autoCorrect: true - ParameterListWrapping: - active: true - autoCorrect: true - indentSize: 4 - SpacingAroundColon: - active: true - autoCorrect: true - SpacingAroundComma: - active: true - autoCorrect: true - SpacingAroundCurly: - active: true - autoCorrect: true - SpacingAroundDot: - active: true - autoCorrect: true - SpacingAroundDoubleColon: - active: true - autoCorrect: true - SpacingAroundKeyword: - active: true - autoCorrect: true - SpacingAroundOperators: - active: true - autoCorrect: true - SpacingAroundParens: - active: true - autoCorrect: true - SpacingAroundRangeOperator: - active: true - autoCorrect: true - SpacingBetweenDeclarationsWithAnnotations: - active: true - autoCorrect: true - SpacingBetweenDeclarationsWithComments: - active: true - autoCorrect: true - StringTemplate: - active: true - autoCorrect: true - -naming: - active: true - ClassNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - classPattern: '[A-Z$][a-zA-Z0-9$]*' - ConstructorParameterNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - parameterPattern: '[a-z][A-Za-z0-9]*' - privateParameterPattern: '[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - ignoreOverridden: true - EnumNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' - ForbiddenClassName: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - forbiddenName: [ ] - FunctionMaxLength: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - maximumFunctionNameLength: 30 - FunctionMinLength: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - minimumFunctionNameLength: 3 - FunctionNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' - excludeClassPattern: '$^' - ignoreOverridden: true - FunctionParameterNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - parameterPattern: '[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - ignoreOverridden: true - - InvalidPackageDeclaration: - active: true - # TODO: Update this with your project's base package - rootPackage: 'template' - - MatchingDeclarationName: - active: true - mustBeFirst: true - MemberNameEqualsClassName: - active: true - ignoreOverridden: true - NonBooleanPropertyPrefixedWithIs: - active: true - ObjectPropertyNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - constantPattern: '[A-Za-z][_A-Za-z0-9]*' - propertyPattern: '[A-Za-z][_A-Za-z0-9]*' - privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' - PackageNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$' - TopLevelPropertyNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - constantPattern: '[A-Z][_A-Z0-9]*' - propertyPattern: '[A-Za-z][_A-Za-z0-9]*' - privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' - VariableMaxLength: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - maximumVariableNameLength: 64 - VariableMinLength: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - minimumVariableNameLength: 1 - VariableNaming: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - variablePattern: '[a-z][A-Za-z0-9]*' - privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' - excludeClassPattern: '$^' - ignoreOverridden: true - -performance: - active: true - ArrayPrimitive: - active: true - ForEachOnRange: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - SpreadOperator: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - UnnecessaryTemporaryInstantiation: - active: true - -potential-bugs: - active: true - Deprecation: - active: true - DuplicateCaseInWhenExpression: - active: true - EqualsAlwaysReturnsTrueOrFalse: - active: true - EqualsWithHashCodeExist: - active: true - ExplicitGarbageCollectionCall: - active: true - HasPlatformType: - active: true - IgnoredReturnValue: - active: true - ImplicitDefaultLocale: - active: false - ImplicitUnitReturnType: - active: true - allowExplicitReturnType: true - InvalidRange: - active: true - IteratorHasNextCallsNextMethod: - active: true - IteratorNotThrowingNoSuchElementException: - active: true - LateinitUsage: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - excludeAnnotatedProperties: [ ] - ignoreOnClassesPattern: '' - MapGetWithNotNullAssertionOperator: - active: true - MissingWhenCase: - active: true - NullableToStringCall: - active: true - RedundantElseInWhen: - active: true - UnconditionalJumpStatementInLoop: - active: true - UnnecessaryNotNullOperator: - active: true - UnnecessarySafeCall: - active: true - UnreachableCode: - active: true - UnsafeCallOnNullableType: - active: true - UnsafeCast: - active: true - UselessPostfixExpression: - active: true - WrongEqualsTypeParameter: - active: true - -style: - active: true - ClassOrdering: - active: true - CollapsibleIfStatements: - active: true - DataClassContainsFunctions: - active: true - conversionFunctionPrefix: 'to' - DataClassShouldBeImmutable: - active: true - EqualsNullCall: - active: true - EqualsOnSignatureLine: - active: true - ExplicitCollectionElementAccessMethod: - active: true - ExplicitItLambdaParameter: - active: true - ExpressionBodySyntax: - active: true - includeLineWrapping: false - ForbiddenComment: - active: false - values: [ 'TODO:', 'FIXME:', 'STOPSHIP:' ] - allowedPatterns: '' - ForbiddenImport: - active: false - imports: [ ] - forbiddenPatterns: '' - ForbiddenMethodCall: - active: false - methods: [ ] - ForbiddenPublicDataClass: - active: false - ignorePackages: [ '*.internal', '*.internal.*' ] - ForbiddenVoid: - active: true - ignoreOverridden: true - ignoreUsageInGenerics: false - FunctionOnlyReturningConstant: - active: true - ignoreOverridableFunction: true - excludedFunctions: 'describeContents' - excludeAnnotatedFunction: [ 'dagger.Provides' ] - LibraryCodeMustSpecifyReturnType: - active: true - LibraryEntitiesShouldNotBePublic: - active: true - LoopWithTooManyJumpStatements: - active: true - maxJumpCount: 3 - MagicNumber: - active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - ignoreNumbers: [ '-1', '0', '1', '2' ] - ignoreHashCodeFunction: true - ignorePropertyDeclaration: false - ignoreLocalVariableDeclaration: false - ignoreConstantDeclaration: true - ignoreCompanionObjectPropertyDeclaration: true - ignoreAnnotation: true - ignoreNamedArgument: true - ignoreEnums: true - ignoreRanges: false - MandatoryBracesIfStatements: - active: true - MandatoryBracesLoops: - active: true - MaxLineLength: - active: true - maxLineLength: 120 - excludePackageStatements: true - excludeImportStatements: true - excludeCommentStatements: false - MayBeConst: - active: true - ModifierOrder: - active: true - NestedClassesVisibility: - active: true - NewLineAtEndOfFile: - active: true - NoTabs: - active: false - OptionalAbstractKeyword: - active: true - OptionalUnit: - active: false - OptionalWhenBraces: - active: true - PreferToOverPairSyntax: - active: true - ProtectedMemberInFinalClass: - active: true - RedundantExplicitType: - active: true - RedundantVisibilityModifierRule: - active: false - ReturnCount: - active: false - max: 2 - excludedFunctions: 'equals' - excludeLabeled: false - excludeReturnFromLambda: true - excludeGuardClauses: false - SafeCast: - active: true - SerialVersionUIDInSerializableClass: - active: true - SpacingBetweenPackageAndImports: - active: true - ThrowsCount: - active: false - max: 2 - TrailingWhitespace: - active: true - UnderscoresInNumericLiterals: - active: true - acceptableDecimalLength: 5 - UnnecessaryAbstractClass: - active: true - excludeAnnotatedClasses: [ 'dagger.Module' ] - UnnecessaryAnnotationUseSiteTarget: - active: true - UnnecessaryApply: - active: true - UnnecessaryInheritance: - active: true - UnnecessaryLet: - active: true - UnnecessaryParentheses: - active: true - UntilInsteadOfRangeTo: - active: true - UnusedImports: - active: true - UnusedPrivateClass: - active: true - UnusedPrivateMember: - active: true - allowedNames: '(_|ignored|expected|serialVersionUID)' - UseArrayLiteralsInAnnotations: - active: true - UseCheckNotNull: - active: true - UseCheckOrError: - active: true - UseDataClass: - active: true - excludeAnnotatedClasses: [ ] - allowVars: false - UseEmptyCounterpart: - active: true - UseIfInsteadOfWhen: - active: true - UseRequire: - active: true - UseRequireNotNull: - active: true - UselessCallOnNotNull: - active: true - UtilityClassWithPublicConstructor: - active: true - VarCouldBeVal: - active: true - WildcardImport: - active: false - excludes: [ '**/test/**', '**/androidTest/**', '**/*.Test.kt', '**/*.Spec.kt', '**/*.Spek.kt' ] - excludeImports: [ 'java.util.*', 'kotlinx.android.synthetic.*' ] diff --git a/gradle.properties b/gradle.properties index e3d93cd..06fa7f9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,9 +5,7 @@ kotlin.incremental=true org.gradle.kotlin.dsl.skipMetadataVersionCheck=false kotlin=1.9.21 -detekt=1.23.4 shadow=8.1.1 -hooks=0.0.2 jvm=17 kordex=1.6.0 serialization=1.6.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index e565e09..0000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,22 +0,0 @@ -[versions] -detekt = "1.23.1" # Note: Plugin versions must be updated in the settings.gradle.kts too -kotlin = "1.9.0" # Note: Plugin versions must be updated in the settings.gradle.kts too - -groovy = "3.0.14" -jansi = "2.4.0" -kord-extensions = "1.5.9-SNAPSHOT" -kx-ser = "1.5.1" -logging = "3.0.5" -logback = "1.4.5" -logback-groovy = "1.14.4" - -[libraries] -detekt = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } -groovy = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" } -jansi = { module = "org.fusesource.jansi:jansi", version.ref = "jansi" } -kord-extensions = { module = "com.kotlindiscord.kord.extensions:kord-extensions", version.ref = "kord-extensions" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } -kx-ser = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kx-ser" } -logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } -logback-groovy = { module = "io.github.virtualdogbert:logback-groovy-config", version.ref = "logback-groovy" } -logging = { module = "io.github.microutils:kotlin-logging", version.ref = "logging" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 36d6c28..522a0be 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,20 +1,13 @@ pluginManagement { val kotlin: String by settings - val detekt: String by settings val shadow: String by settings - val hooks: String by settings plugins { - // Update this in libs.version.toml when you change it here. kotlin("jvm") version kotlin kotlin("plugin.serialization") version kotlin - // Update this in libs.version.toml when you change it here. - id("io.gitlab.arturbosch.detekt") version detekt - - id("com.github.jakemarsden.git-hooks") version hooks id("com.github.johnrengelman.shadow") version shadow } } -rootProject.name = "supertrouper" +rootProject.name = "Trouper" diff --git a/src/main/kotlin/quest/laxla/supertrouper/AboutExtension.kt b/src/main/kotlin/quest/laxla/supertrouper/AboutExtension.kt deleted file mode 100644 index 066c89a..0000000 --- a/src/main/kotlin/quest/laxla/supertrouper/AboutExtension.kt +++ /dev/null @@ -1,38 +0,0 @@ -package quest.laxla.supertrouper - -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand -import dev.kord.common.entity.Permission -import dev.kord.common.entity.Snowflake - -class AboutExtension : Extension() { - override val name: String - get() = "about" - - override suspend fun setup() { - ephemeralSlashCommand { - name = "about" - - action { - respond { - //language=Markdown - content = "Hey! This is a test command. It's powered by *magic*:sparkles:" - } - } - } - - publicSlashCommand { - name = "stop" - description = "WARNING: Stops the bot completely." - guildId = Snowflake(officialServer) - requirePermission(Permission.Administrator) - - action { - //language=Markdown - respond { content = "# Invoking Protocol: Emergency Stop" } - bot.stop() - } - } - } -} diff --git a/src/main/kotlin/quest/laxla/supertrouper/App.kt b/src/main/kotlin/quest/laxla/supertrouper/App.kt deleted file mode 100644 index 7afa490..0000000 --- a/src/main/kotlin/quest/laxla/supertrouper/App.kt +++ /dev/null @@ -1,20 +0,0 @@ -package quest.laxla.supertrouper - -import com.kotlindiscord.kord.extensions.ExtensibleBot -import com.kotlindiscord.kord.extensions.utils.env -import kotlinx.coroutines.runBlocking - -private val token = env("token") -val officialServer = env("official_server") - -fun main() = runBlocking { - ExtensibleBot(token) { - extensions { - add(::AboutExtension) - } - - applicationCommands { - defaultGuild(officialServer) - } - }.start() -} diff --git a/src/main/kotlin/quest/laxla/trouper/AboutExtension.kt b/src/main/kotlin/quest/laxla/trouper/AboutExtension.kt new file mode 100644 index 0000000..fbdd479 --- /dev/null +++ b/src/main/kotlin/quest/laxla/trouper/AboutExtension.kt @@ -0,0 +1,122 @@ +package quest.laxla.trouper + +import com.kotlindiscord.kord.extensions.components.components +import com.kotlindiscord.kord.extensions.components.disabledButton +import com.kotlindiscord.kord.extensions.components.linkButton +import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand +import dev.kord.common.entity.ButtonStyle +import dev.kord.core.entity.effectiveName +import dev.kord.rest.builder.message.allowedMentions +import dev.kord.rest.builder.message.create.AbstractMessageCreateBuilder +import dev.kord.rest.builder.message.embed +import quest.laxla.trouper.messaging.PrivateMessagesCategoryName + +class AboutExtension : TrouperExtension() { + override suspend fun setup() { + ephemeralSlashCommand { + name = "about" + description = "About Super Trouper" + + action { + respond { + about() + } + } + } + } + + suspend fun AbstractMessageCreateBuilder.about() { + val self = kord.getSelf() + val avatar = (self.avatar ?: self.defaultAvatar).cdnUrl.toUrl() + val mention = self.mention + allowedMentions() + + embed { + title = "About ${self.effectiveName}" + if (version == null) "" else " `$version`" + thumbnail { url = avatar } + + if (isDevelopmentEnvironment) field { + name = "Development Environment" + //language=Markdown + value = "> This instance is hosted on someone's personal computer, " + + "and *may* contain **malicious code** and/or **steal your data**. " + + "This is not considered to be an official version of Trouper. " + + "Do *not* rely on the lack of this message to determine if an instance is official." + } + + //language=Markdown + description = "$mention is an *open-source* bot made by the plural community, for the plural community.\n\n" + + "$mention creates a private channel for members, " + + "allowing them to talk to the server's owners or moderators. " + + "Use `/pm` to get a link to your PM channel. " + + "PM channels inherit their permissions from the `$PrivateMessagesCategoryName` category, " + + "and can be synced by moderators (`Manage Permissions` is required by default).\n\n" + + "$mention is free, open source software. You can host the bot on your own server, " + + "without paying us a penny. We know not everyone can afford that, so we host it for you, " + + "using our own money. Please, help us making $mention available for everyone, everywhere, for free." + } + + components { + val app = kord.getApplicationInfo() + + disabledButton { + style = ButtonStyle.Primary + label = "About" + } + + app.inviteUrl?.let { + linkButton { + url = it + label = "Invite" + } + } + + officialServerUrl?.let { + linkButton { + url = it + label = "Join" + } + } + + donateUrl?.let { + linkButton { + url = it + label = "Donate" + } + } + + repoUrl?.let { + linkButton { + url = it + label = "Contribute" + } + } + + disabledButton(row = 1) { + style = ButtonStyle.Primary + label = "Legal" + } + + licenseUrl?.let { + linkButton(row = 1) { + url = it + label = license ?: "License" + } + } + + app.privacyPolicyUrl?.let { + linkButton(row = 1) { + url = it + label = "Privacy Policy" + } + } + + app.termsOfServiceUrl?.let { + linkButton(row = 1) { + url = it + label = "Terms of Service" + } + } + } + } +} diff --git a/src/main/kotlin/quest/laxla/trouper/App.kt b/src/main/kotlin/quest/laxla/trouper/App.kt new file mode 100644 index 0000000..ae09eda --- /dev/null +++ b/src/main/kotlin/quest/laxla/trouper/App.kt @@ -0,0 +1,33 @@ +package quest.laxla.trouper + +import com.kotlindiscord.kord.extensions.ExtensibleBot +import com.kotlindiscord.kord.extensions.utils.env +import com.kotlindiscord.kord.extensions.utils.envOrNull +import dev.kord.gateway.PrivilegedIntent +import kotlinx.coroutines.runBlocking +import quest.laxla.trouper.messaging.PrivateMassagingExtension + +private val token = env("TOKEN") +val officialServer = env("OFFICIAL_SERVER") +val isDevelopmentEnvironment = envOrNull("IS_DEV_ENV").toBoolean() +val officialServerUrl = envOrNull("OFFICIAL_SERVER_URL") +val license = envOrNull("LICENSE") +val licenseUrl = envOrNull("LICENSE_URL") +val donateUrl = envOrNull("DONATE_URL") +val repoUrl = envOrNull("REPO_URL") +val version = AboutExtension::class.java.getResourceAsStream("/.version")?.bufferedReader()?.use { it.readText() } + +fun main() = runBlocking { + ExtensibleBot(token) { + applicationCommands { + if (isDevelopmentEnvironment) defaultGuild(officialServer) + } + + extensions { + add(::MaintenanceExtension) + @OptIn(PrivilegedIntent::class) + add(::PrivateMassagingExtension) + add(::AboutExtension) + } + }.start() +} diff --git a/src/main/kotlin/quest/laxla/trouper/MaintenanceExtension.kt b/src/main/kotlin/quest/laxla/trouper/MaintenanceExtension.kt new file mode 100644 index 0000000..017b72b --- /dev/null +++ b/src/main/kotlin/quest/laxla/trouper/MaintenanceExtension.kt @@ -0,0 +1,26 @@ +package quest.laxla.trouper + +import com.kotlindiscord.kord.extensions.checks.isBotAdmin +import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand +import com.kotlindiscord.kord.extensions.extensions.slashCommandCheck +import dev.kord.common.entity.Snowflake + +class MaintenanceExtension : TrouperExtension() { + override suspend fun setup() { + slashCommandCheck { + isBotAdmin() + } + + publicSlashCommand { + name = "stop" + description = "Stops the bot completely" + guildId = Snowflake(officialServer) + + action { + //language=Markdown + respond { content = "# Invoking Protocol: Emergency Stop" } + bot.stop() + } + } + } +} diff --git a/src/main/kotlin/quest/laxla/trouper/Overwrites.kt b/src/main/kotlin/quest/laxla/trouper/Overwrites.kt new file mode 100644 index 0000000..c5aff62 --- /dev/null +++ b/src/main/kotlin/quest/laxla/trouper/Overwrites.kt @@ -0,0 +1,55 @@ +package quest.laxla.trouper + +import dev.kord.common.entity.Overwrite +import dev.kord.common.entity.OverwriteType +import dev.kord.common.entity.Permissions +import dev.kord.common.entity.Snowflake +import dev.kord.core.entity.PermissionOverwriteEntity +import dev.kord.rest.builder.channel.PermissionOverwritesBuilder + +fun overwrite( + target: Snowflake, + type: OverwriteType, + allowed: Permissions = Permissions(), + denied: Permissions = Permissions() +) = Overwrite(target, type, allowed, denied) + +fun PermissionOverwritesBuilder.addOverwrite( + target: Snowflake, + type: OverwriteType, + allowed: Permissions = Permissions(), + denied: Permissions = Permissions() +) = addOverwrite(overwrite(target, type, allowed, denied)) + +fun PermissionOverwritesBuilder.sync( + vararg overrides: Overwrite, + defaults: Iterable, + neverAllow: Permissions = Permissions() +) = sync(overrides.asIterable(), defaults, neverAllow) + +fun PermissionOverwritesBuilder.sync( + overrides: Iterable, + defaults: Iterable, + neverAllow: Permissions = Permissions() +) { + val permissions = mutableMapOf() + + defaults.forEach { default -> + val override = overrides.find { it.id == default.target && it.type == default.type } + + if (override == null) addOverwrite(default.target, default.type, default.allowed - neverAllow, default.denied) + else permissions[override] = default + } + + overrides.forEach { override -> + val default = permissions[override] + + if (default == null) addOverwrite(override.copy(allow = override.allow - neverAllow)) + else addOverwrite( + default.target, + default.type, + default.allowed - default.denied + override.allow - override.deny - neverAllow, + default.denied - default.allowed - override.allow + override.deny + ) + } +} diff --git a/src/main/kotlin/quest/laxla/trouper/TargetedArguments.kt b/src/main/kotlin/quest/laxla/trouper/TargetedArguments.kt new file mode 100644 index 0000000..09a1a99 --- /dev/null +++ b/src/main/kotlin/quest/laxla/trouper/TargetedArguments.kt @@ -0,0 +1,19 @@ +package quest.laxla.trouper + +import com.kotlindiscord.kord.extensions.commands.Arguments +import com.kotlindiscord.kord.extensions.commands.application.slash.SlashCommandContext +import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalMember +import com.kotlindiscord.kord.extensions.components.forms.ModalForm + +private const val TargetArgumentName = "target" +private const val TargetArgumentDescription = "Target of this command. Defaults to you." + +open class TargetedArguments : Arguments() { + val targetOrNull by optionalMember { + name = TargetArgumentName + description = TargetArgumentDescription + } +} + +val C.target where C : SlashCommandContext<*, A, M>, A : TargetedArguments, M : ModalForm + get() = arguments.targetOrNull ?: member!! diff --git a/src/main/kotlin/quest/laxla/trouper/TrouperExtension.kt b/src/main/kotlin/quest/laxla/trouper/TrouperExtension.kt new file mode 100644 index 0000000..1435c21 --- /dev/null +++ b/src/main/kotlin/quest/laxla/trouper/TrouperExtension.kt @@ -0,0 +1,10 @@ +package quest.laxla.trouper + +import com.kotlindiscord.kord.extensions.extensions.Extension + +private const val NameRegexGroup = "name" + +abstract class TrouperExtension : Extension() { + final override val name: String = this::class.simpleName!!.substringBeforeLast("Extension") + .replace("(?<$NameRegexGroup>[A-Z])".toRegex()) { '-' + it.groups[NameRegexGroup]!!.value.lowercase() }.removePrefix("-") +} diff --git a/src/main/kotlin/quest/laxla/trouper/Utils.kt b/src/main/kotlin/quest/laxla/trouper/Utils.kt new file mode 100644 index 0000000..ccbca93 --- /dev/null +++ b/src/main/kotlin/quest/laxla/trouper/Utils.kt @@ -0,0 +1,27 @@ +package quest.laxla.trouper + +import dev.kord.core.entity.Application +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.text.buildString as buildStringKt + +@OptIn(ExperimentalContracts::class) +inline fun T.buildString(capacity: Int? = null, builderAction: StringBuilder.(T) -> Unit): String { + contract { + callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) + } + + return if (capacity == null) buildStringKt { builderAction(this@buildString) } + else buildStringKt(capacity) { builderAction(this@buildString) } +} + + +val Application.inviteUrl get() = customInstallUrl ?: installParams?.buildString { + append("https://discord.com/api/oauth2/authorize?client_id=") + append(id) + append("&permissions=") + append(it.permissions.code.value) + append("&scope=") + append(it.scopes.joinToString(separator = "+")) +} diff --git a/src/main/kotlin/quest/laxla/trouper/messaging/PrivateMassagingExtension.kt b/src/main/kotlin/quest/laxla/trouper/messaging/PrivateMassagingExtension.kt new file mode 100644 index 0000000..dac888b --- /dev/null +++ b/src/main/kotlin/quest/laxla/trouper/messaging/PrivateMassagingExtension.kt @@ -0,0 +1,189 @@ +package quest.laxla.trouper.messaging + +import com.kotlindiscord.kord.extensions.checks.anyGuild +import com.kotlindiscord.kord.extensions.checks.isNotBot +import com.kotlindiscord.kord.extensions.extensions.* +import com.kotlindiscord.kord.extensions.types.EphemeralInteractionContext +import dev.kord.common.entity.ButtonStyle +import dev.kord.common.entity.DiscordPartialEmoji +import dev.kord.common.entity.OverwriteType +import dev.kord.common.entity.Permission +import dev.kord.core.behavior.GuildBehavior +import dev.kord.core.behavior.UserBehavior +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.channel.createTextChannel +import dev.kord.core.behavior.channel.edit +import dev.kord.core.behavior.createCategory +import dev.kord.core.behavior.interaction.respondPublic +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.Category +import dev.kord.core.entity.channel.TextChannel +import dev.kord.core.event.guild.MemberJoinEvent +import dev.kord.core.event.interaction.GuildButtonInteractionCreateEvent +import dev.kord.gateway.Intent +import dev.kord.gateway.PrivilegedIntent +import dev.kord.rest.builder.channel.addMemberOverwrite +import dev.kord.rest.builder.channel.addRoleOverwrite +import dev.kord.rest.builder.message.actionRow +import dev.kord.rest.builder.message.embed +import kotlinx.coroutines.flow.count +import quest.laxla.trouper.* + +@PrivilegedIntent +class PrivateMassagingExtension : TrouperExtension() { + override suspend fun setup() { + intents += Intent.GuildMembers + + slashCommandCheck { anyGuild(); isNotBot() } + userCommandCheck { anyGuild(); isNotBot() } + + event { + action { + if (event.member.isEligible && event.guild.members.count() < memberLimit) + getOrCreateChannel(getOrCreateCategory(event.guild), event.member) + } + } + + ephemeralSlashCommand(::TargetedArguments) { + name = "pm" + description = "Get a link to a user's private messages channel" + + action { + executeFindCommand(getOrCreateCategory(guild!!), target.asUser(), user) + } + } + + ephemeralUserCommand { + name = "Private Message" + + action { + executeFindCommand(getOrCreateCategory(guild!!), targetUsers.single(), user) + } + } + + ephemeralSlashCommand(::TargetedArguments) { + name = "sync" + description = "Syncs a private message channel's permissions with the category" + + requirePermission(Permission.ManageRoles) + + action { + executeSyncCommand(getOrCreateCategory(guild!!), target.asUser()) + } + } + + ephemeralUserCommand { + name = "Sync PM Channel" + + requirePermission(Permission.ManageRoles) + + action { + executeSyncCommand(getOrCreateCategory(guild!!), user.asUser()) + } + } + + event { + action { + when (event.interaction.componentId) { + PingButton -> event.interaction.respondPublic { + executePingCommand(event.interaction.channel.asChannel(), event.interaction.user) + } + } + } + } + } + + private suspend fun EphemeralInteractionContext.executeSyncCommand(category: Category, user: User) { + val channel = getChannel(category, user) + if (channel == null) { + respond { content = "${user.mention} does not have a private messaging channel in this server." } + return + } + + val userMention = user.mention + val channelMention = channel.mention + + channel.edit { + reason = "Sync $channelMention with category for $userMention" + + sync( + overwrite(kord.selfId, OverwriteType.Member, allowed = pmBotPermissions), + overwrite(user.id, OverwriteType.Member, allowed = pmMemberPermissions), + defaults = category.permissionOverwrites, + neverAllow = kord.getSelf().asMember(category.guildId).getDeniedPermissions() + ) + } + + respond { + content = "Synced $channelMention for $userMention successfully." + } + } + + private suspend fun EphemeralInteractionContext.executeFindCommand( + category: Category, user: User, searcher: UserBehavior = user + ) { + if (user.isEligible) { + val channel = getOrCreateChannel(category, user) + respond { content = channel.mention } + + channel.ping(searcher) + } else respond { + content = user.mention + " is not eligible for private messaging." + } + } + + private suspend fun getOrCreateCategory(guild: GuildBehavior) = getCategory(guild) ?: createCategory(guild) + + private suspend fun createCategory(guild: GuildBehavior) = guild.createCategory(PrivateMessagesCategoryName) { + reason = "Private messaging category was missing." + nsfw = false + + addMemberOverwrite(kord.selfId) { + allowed += pmBotPermissions + } + + addRoleOverwrite(guild.id) { + denied += Permission.ViewChannel + } + } + + private suspend fun getOrCreateChannel(category: Category, user: User) = + getChannel(category, user) ?: createChannel(category, user) + + private suspend fun createChannel(category: Category, user: User): TextChannel { + val mention = user.mention + + val channel = category.createTextChannel(user.username) { + reason = "Created a PM with $mention." + nsfw = category.data.nsfw.discordBoolean + topic = "$mention's private messaging channel." + + sync( + overwrite(kord.selfId, OverwriteType.Member, allowed = pmBotPermissions), + overwrite(user.id, OverwriteType.Member, allowed = pmMemberPermissions), + defaults = category.permissionOverwrites, + neverAllow = kord.getSelf().asMember(category.guildId).getDeniedPermissions() + ) + } + + val avatar = (user.avatar ?: user.defaultAvatar).cdnUrl.toUrl() + + channel.createMessage { + embed { + description = "# $mention" + thumbnail { url = avatar } + } + + actionRow { + interactionButton(ButtonStyle.Primary, customId = PingButton) { + label = "Ping" + emoji = DiscordPartialEmoji(name = "\uD83D\uDD14") + } + } + } + + channel.ping(user) + + return channel + } +} diff --git a/src/main/kotlin/quest/laxla/trouper/messaging/PrivateMessaging.kt b/src/main/kotlin/quest/laxla/trouper/messaging/PrivateMessaging.kt new file mode 100644 index 0000000..48d659d --- /dev/null +++ b/src/main/kotlin/quest/laxla/trouper/messaging/PrivateMessaging.kt @@ -0,0 +1,74 @@ +package quest.laxla.trouper.messaging + +import com.kotlindiscord.kord.extensions.utils.envOrNull +import dev.kord.common.entity.ALL +import dev.kord.common.entity.Permission +import dev.kord.common.entity.Permissions +import dev.kord.common.entity.Snowflake +import dev.kord.core.behavior.GuildBehavior +import dev.kord.core.behavior.UserBehavior +import dev.kord.core.behavior.channel.MessageChannelBehavior +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.entity.Member +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.Category +import dev.kord.core.entity.channel.MessageChannel +import dev.kord.core.entity.channel.TextChannel +import dev.kord.rest.builder.message.allowedMentions +import dev.kord.rest.builder.message.create.AbstractMessageCreateBuilder +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.firstOrNull + +const val PrivateMessagesCategoryName = "Private Messages" +const val PingButton = "PM.ping" +const val UserIdCapturingGroup = "userID" +val memberLimit = envOrNull("AUTOMATIC_CHANNEL_CREATION_MEMBER_LIMIT")?.toInt() ?: 30 +val pmMemberPermissions = Permission.ViewChannel + Permission.ReadMessageHistory +val pmBotPermissions = + pmMemberPermissions + Permission.ManageChannels + Permission.SendMessages + Permission.ManageMessages +val userMentionRegex = "<@(?<$UserIdCapturingGroup>[1-9][0-9]+)>".toRegex() + +infix fun TextChannel.isOf(user: UserBehavior) = topic?.contains(user.mention) == true + +suspend fun getChannel(category: Category, user: User) = category.channels.filterIsInstance().firstOrNull { + it.categoryId == category.id && it isOf user +} + +suspend fun getCategory(guild: GuildBehavior) = + guild.channels.filterIsInstance().firstOrNull { it.isUsableForPrivateMessaging } + +val User.isEligible get() = !isBot +val Category.isUsableForPrivateMessaging get() = name.equals(PrivateMessagesCategoryName, ignoreCase = true) + +fun AbstractMessageCreateBuilder.executePingCommand( + channel: MessageChannel, + pinger: UserBehavior +) { + val owners = channel.owners?.toList() + + if (owners == null) { + allowedMentions() + + content = "The owner of this channel is unknown. They need to be mentioned in the channel's topic, " + + "Like this: `<@userID>`." + } else { + allowedMentions { + users.addAll(owners.asSequence().map { + Snowflake(it.groups[UserIdCapturingGroup]!!.value) + }) + } + + content = "Hey, " + owners.joinToString(separator = " ") { + it.value + } + ", y'all were pinged by " + pinger.mention + '!' + } +} + +suspend fun MessageChannelBehavior.ping(user: UserBehavior) = createMessage { + allowedMentions { users.add(user.id) } + content = user.mention +}.delete(reason = "Ghost pinged " + user.mention) + +val MessageChannel.owners get() = data.topic.value?.let { userMentionRegex.findAll(it) } + +suspend fun Member.getDeniedPermissions() = Permissions.ALL - getPermissions()