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()