diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..29c5b052 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +enable-beta-ecosystems: true + +updates: + - package-ecosystem: "gradle" + directory: "/" + + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 263b2dde..b63888ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: uses: actions/setup-java@v1 with: - java-version: 1.16 + java-version: 1.17 - name: Gradle (Build) uses: gradle/gradle-build-action@v2 diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index a60d0414..ec7ac137 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -19,7 +19,7 @@ jobs: uses: actions/setup-java@v1 with: - java-version: 1.16 + java-version: 1.17 - name: Gradle (Build) uses: gradle/gradle-build-action@v2 diff --git a/.github/workflows/root.yml b/.github/workflows/root.yml index 0893be7d..0e44fed8 100644 --- a/.github/workflows/root.yml +++ b/.github/workflows/root.yml @@ -20,7 +20,7 @@ jobs: uses: actions/setup-java@v1 with: - java-version: 1.16 + java-version: 1.17 - name: Gradle (Build) uses: gradle/gradle-build-action@v2 diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index ae3f30ae..8d81632f 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 9c0556c8..e9f4dbf7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,7 +5,7 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..128cf299 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/module-log-parser/CozyDiscord.module-log-parser.test.iml b/.idea/modules/module-log-parser/CozyDiscord.module-log-parser.test.iml deleted file mode 100644 index e69de29b..00000000 diff --git a/build.gradle.kts b/build.gradle.kts index 821e93b0..c5db4f47 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -69,7 +69,9 @@ dependencies { implementation(libs.kordex.mappings) implementation(libs.kordex.phishing) implementation(libs.kordex.pluralkit) + implementation(libs.kordex.tags) implementation(libs.kordex.unsafe) + implementation(libs.kordex.welcome) implementation(libs.commons.text) implementation(libs.homoglyph) @@ -91,9 +93,7 @@ dependencies { implementation(projects.moduleLogParser) implementation(projects.moduleModeration) implementation(projects.moduleRoleSync) - implementation(projects.moduleTags) implementation(projects.moduleUserCleanup) - implementation(projects.moduleWelcome) } graphql { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 257a77c5..d24f9c2a 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -17,15 +17,15 @@ dependencies { implementation(gradleApi()) implementation(localGroovy()) - implementation(kotlin("gradle-plugin", version = "1.9.21")) - implementation(kotlin("serialization", version = "1.9.21")) + implementation(kotlin("gradle-plugin", version = "1.9.22")) + implementation(kotlin("serialization", version = "1.9.22")) implementation("gradle.plugin.org.cadixdev.gradle", "licenser", "0.6.1") implementation("com.github.jakemarsden", "git-hooks-gradle-plugin", "0.0.2") - implementation("com.google.devtools.ksp", "com.google.devtools.ksp.gradle.plugin", "1.9.21-1.0.15") - implementation("io.gitlab.arturbosch.detekt", "detekt-gradle-plugin", "1.23.4") + implementation("com.google.devtools.ksp", "com.google.devtools.ksp.gradle.plugin", "1.9.22-1.0.17") + implementation("io.gitlab.arturbosch.detekt", "detekt-gradle-plugin", "1.23.5") // implementation("org.ec4j.editorconfig", "org.ec4j.editorconfig.gradle.plugin", "0.0.3") - implementation("com.expediagroup.graphql", "com.expediagroup.graphql.gradle.plugin", "6.3.5") + implementation("com.expediagroup.graphql", "com.expediagroup.graphql.gradle.plugin", "7.0.2") implementation("com.github.johnrengelman.shadow", "com.github.johnrengelman.shadow.gradle.plugin", "8.1.1") } diff --git a/buildSrc/src/main/kotlin/cozy-module.gradle.kts b/buildSrc/src/main/kotlin/cozy-module.gradle.kts index 668cb9e8..27ae4350 100644 --- a/buildSrc/src/main/kotlin/cozy-module.gradle.kts +++ b/buildSrc/src/main/kotlin/cozy-module.gradle.kts @@ -109,7 +109,7 @@ tasks { detekt { buildUponDefaultConfig = true - config = rootProject.files("detekt.yml") + config.from(rootProject.files("detekt.yml")) } // Credit to ZML for this workaround. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5e0537c..460ba6d5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,27 +1,27 @@ [versions] -detekt = "1.23.4" -kotlin = "1.9.21" -graphql = "6.3.5" +detekt = "1.23.5" +kotlin = "1.9.22" +graphql = "7.0.2" -autolink = "0.10.1" -commons-text = "1.10.0" +autolink = "0.11.0" +commons-text = "1.11.0" excelkt = "1.0.2" flexver = "1.1.1" -groovy = "3.0.19" +groovy = "3.0.20" homoglyph = "1.2.1" jansi = "2.4.1" -jsoup = "1.17.1" -kaml = "0.55.0" -kmongo = "4.8.0" -kordex = "1.7.1-SNAPSHOT" +jsoup = "1.17.2" +kaml = "0.57.0" +kmongo = "4.11.0" +kordex = "1.8.0-SNAPSHOT" kotlintest = "3.4.2" -ktor = "2.2.4" -kx-ser = "1.6.2" -logback = "1.4.14" +ktor = "2.3.8" +kx-ser = "1.6.3" +logback = "1.5.1" logback-groovy = "1.14.5" -logging = "5.1.1" +logging = "6.0.3" moshi = "1.15.0" -rgxgen = "1.4" +rgxgen = "2.0" semver = "1.4.2" [libraries] @@ -45,6 +45,8 @@ kordex-mappings = { module = "com.kotlindiscord.kord.extensions:extra-mappings", kordex-phishing = { module = "com.kotlindiscord.kord.extensions:extra-phishing", version.ref = "kordex" } kordex-pluralkit = { module = "com.kotlindiscord.kord.extensions:extra-pluralkit", version.ref = "kordex" } kordex-unsafe = { module = "com.kotlindiscord.kord.extensions:unsafe", version.ref = "kordex" } +kordex-tags = { module = "com.kotlindiscord.kord.extensions:extra-tags", version.ref = "kordex" } +kordex-welcome = { module = "com.kotlindiscord.kord.extensions:extra-welcome", version.ref = "kordex" } kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3499ded5..509c4a29 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/module-tags/build.gradle.kts b/module-tags/build.gradle.kts deleted file mode 100644 index 4abbcd4e..00000000 --- a/module-tags/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -plugins { - `api-module` - `cozy-module` - `published-module` -} - -dependencies { - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) - - ksp(libs.kordex.annotationProcessor) - - implementation(libs.kordex.annotations) - implementation(libs.kordex.core) - implementation(libs.kordex.unsafe) - - implementation(libs.logging) - - implementation(platform(libs.kotlin.bom)) - implementation(libs.kotlin.stdlib) -} diff --git a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/TagsExtension.kt b/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/TagsExtension.kt deleted file mode 100644 index 8f13bd03..00000000 --- a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/TagsExtension.kt +++ /dev/null @@ -1,630 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome - -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.* -import com.kotlindiscord.kord.extensions.components.forms.ModalForm -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand -import com.kotlindiscord.kord.extensions.modules.unsafe.annotations.UnsafeAPI -import com.kotlindiscord.kord.extensions.modules.unsafe.extensions.unsafeSubCommand -import com.kotlindiscord.kord.extensions.modules.unsafe.types.InitialSlashCommandResponse -import com.kotlindiscord.kord.extensions.modules.unsafe.types.ackEphemeral -import com.kotlindiscord.kord.extensions.modules.unsafe.types.respondEphemeral -import com.kotlindiscord.kord.extensions.utils.FilterStrategy -import com.kotlindiscord.kord.extensions.utils.suggestStringMap -import dev.kord.common.annotation.KordUnsafe -import dev.kord.core.behavior.channel.createMessage -import dev.kord.core.behavior.interaction.modal -import dev.kord.core.behavior.interaction.suggestString -import dev.kord.rest.builder.message.allowedMentions -import org.koin.core.component.inject -import org.quiltmc.community.cozy.modules.tags.config.TagsConfig -import org.quiltmc.community.cozy.modules.tags.data.Tag -import org.quiltmc.community.cozy.modules.tags.data.TagsData -import org.quiltmc.community.cozy.modules.tags.nullIfBlank - -internal const val POSITIVE_EMOTE = "\uD83D\uDC4D" -internal const val NEGATIVE_EMOTE = "❌" - -@OptIn(UnsafeAPI::class) -@Suppress("MagicNumber") -public class TagsExtension : Extension() { - override val name: String = "quiltmc-tags" - - public val tagsConfig: TagsConfig by inject() - public val tagsData: TagsData by inject() - - override suspend fun setup() { - publicSlashCommand(::GetTagArgs) { - name = "tag" - description = "Retrieve a tag and send it" - - tagsConfig.getUserCommandChecks().forEach(::check) - - action { - val tag = tagsData.getTagByKey(arguments.tagKey, guild?.id) - - if (tag == null) { - respond { - content = "$NEGATIVE_EMOTE Unknown tag: ${arguments.tagKey}" - } - - return@action - } - - respond { - tagsConfig.getTagFormatter() - .invoke(this, tag) - - if (arguments.userToMention != null) { - content = "${arguments.userToMention!!.mention}\n\n${content ?: ""}" - - allowedMentions { - users += arguments.userToMention!!.id - } - } - } - } - } - - ephemeralSlashCommand { - name = "list-tags" - description = "Commands for listing tags by various criteria" - - tagsConfig.getUserCommandChecks().forEach(::check) - - ephemeralSubCommand(::ByCategoryArgs) { - name = "by-category" - description = "List tags by matching their category" - - action { - val tags = tagsData.getTagsByCategory(arguments.category, guild?.id) - - if (tags.isEmpty()) { - respond { - content = "$NEGATIVE_EMOTE Tag not found" - } - - return@action - } - - editingPaginator { - timeoutSeconds = 60 - - tags.forEach { tag -> - page { - title = tag.title - description = tag.description - color = tag.color - - footer { - text = "${tag.category}/${tag.key}" - } - - image = tag.image - } - } - }.send() - } - } - - ephemeralSubCommand(::ByKeyArgs) { - name = "by-key" - description = "List tags by matching their key" - - action { - val tags = tagsData.getTagsByPartialKey(arguments.key, guild?.id) - - if (tags.isEmpty()) { - respond { - content = "$NEGATIVE_EMOTE Tag not found" - } - - return@action - } - - editingPaginator { - timeoutSeconds = 60 - - tags.forEach { tag -> - page { - title = "tag.title" - description = tag.description - color = tag.color - - footer { - text = "${tag.category}/${tag.key}" - } - - image = tag.image - } - } - }.send() - } - } - - ephemeralSubCommand(::ByTitleArgs) { - name = "by-title" - description = "List tags by matching their title" - - action { - val tags = tagsData.getTagsByPartialTitle(arguments.title, guild?.id) - - if (tags.isEmpty()) { - respond { - content = "$NEGATIVE_EMOTE Tag not found" - } - - return@action - } - - editingPaginator { - timeoutSeconds = 60 - - tags.forEach { tag -> - page { - title = "tag.title" - description = tag.description - color = tag.color - - footer { - text = "${tag.category}/${tag.key}" - } - - image = tag.image - } - } - }.send() - } - } - } - - ephemeralSlashCommand { - name = "manage-tags" - description = "Tag management commands" - - allowInDms = false - - check { - anyGuild() - } - - tagsConfig.getStaffCommandChecks().forEach(::check) - - @OptIn(KordUnsafe::class) - unsafeSubCommand(::SetArgs) { - name = "set" - description = "Create or replace a tag" - - initialResponse = InitialSlashCommandResponse.None - - action { - val modalObj = TagEditModal() - - this@unsafeSubCommand.componentRegistry.register(modalObj) - - event.interaction.modal( - modalObj.translateTitle(getLocale(), bundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, getLocale(), bundle) - } - - interactionResponse = modalObj.awaitCompletion { - it?.deferEphemeralResponseUnsafe() - } ?: return@action - - val tag = Tag( - category = arguments.category, - description = modalObj.description.value!!, - key = arguments.key, - title = modalObj.tagTitle.value!!, - color = arguments.colour, - guildId = arguments.guild?.id, - image = modalObj.imageUrl.value.nullIfBlank() - ) - - tagsData.setTag(tag) - - tagsConfig.getLoggingChannel(guild!!.asGuild()).createMessage { - allowedMentions { } - - content = "**Tag created/updated by ${user.mention}**\n\n" - - tagsConfig.getTagFormatter().invoke(this, tag) - } - - respondEphemeral { - content = "$POSITIVE_EMOTE Tag set: ${tag.title}" - } - } - } - - @OptIn(KordUnsafe::class) - unsafeSubCommand(::EditArgs) { - name = "edit" - description = "Edit an existing tag" - - initialResponse = InitialSlashCommandResponse.None - - action { - var tag = tagsData.getTagByKey(arguments.key, arguments.guild?.id) - - if (tag == null) { - ackEphemeral { - content = "$NEGATIVE_EMOTE Tag not found" - } - - return@action - } - - val modalObj = TagEditModal( - true, - tag.key, - tag.title, - tag.description, - tag.image - ) - - this@unsafeSubCommand.componentRegistry.register(modalObj) - - event.interaction.modal( - modalObj.translateTitle(getLocale(), bundle), - modalObj.id - ) { - modalObj.applyToBuilder(this, getLocale(), bundle) - } - - interactionResponse = modalObj.awaitCompletion { - it?.deferEphemeralResponseUnsafe() - } ?: return@action - - if (!modalObj.tagTitle.value.isNullOrBlank()) { - tag = tag.copy(title = modalObj.tagTitle.value!!) - } - - if (!modalObj.description.value.isNullOrBlank()) { - tag = tag.copy(description = modalObj.description.value!!) - } - - if (arguments.category != null) { - tag = tag.copy(category = arguments.category!!) - } - - if (arguments.colour != null) { - tag = tag.copy(color = arguments.colour!!) - } - - if (modalObj.imageUrl.value.isNullOrBlank()) { - tag = tag.copy(image = null) - } else { - tag = tag.copy(image = modalObj.imageUrl.value) - } - - tagsData.setTag(tag) - - tagsConfig.getLoggingChannel(guild!!.asGuild()).createMessage { - allowedMentions { } - - content = "**Tag edited by ${user.mention}**\n\n" - - tagsConfig.getTagFormatter().invoke(this, tag) - } - - respondEphemeral { - content = "$POSITIVE_EMOTE Tag edited: ${tag.title}" - } - } - } - - ephemeralSubCommand(::FindArgs) { - name = "find" - description = "Find tags, by the given key and guild ID" - - action { - val tags = tagsData.findTags( - category = arguments.category, - guildId = arguments.guild?.id, - key = arguments.key - ) - - if (tags.isEmpty()) { - respond { - content = "$NEGATIVE_EMOTE No tags found for that query." - } - - return@action - } - - editingPaginator { - timeoutSeconds = 60 - - val chunks = tags.chunked(10) - - chunks.forEach { chunk -> - page { - description = chunk.joinToString("\n\n") { - """ - **Key:** `${it.key}` - **Title:** `${it.title}` - **Category:** `${it.category}` - **Guild ID:** `${it.guildId ?: "N/A"}` - **Image:** `${it.image ?: "N/A"}` - """.trimIndent() - } - } - } - }.send() - } - } - - ephemeralSubCommand(::ByKeyAndOptionalGuildArgs) { - name = "delete" - description = "Delete a tag, by key and guild ID" - - action { - val tag = tagsData.deleteTagByKey(arguments.key, arguments.guild?.id) - - if (tag != null) { - tagsConfig.getLoggingChannel(guild!!.asGuild()).createMessage { - allowedMentions { } - - content = "**Tag removed by ${user.mention}**\n\n" - - tagsConfig.getTagFormatter().invoke(this, tag) - } - } - - respond { - content = if (tag == null) { - "$NEGATIVE_EMOTE Tag not found" - } else { - "$POSITIVE_EMOTE Deleted tag: ${tag.title}" - } - } - } - } - } - } - - // region: Arguments - - private fun GetTagArgs(): GetTagArgs = - GetTagArgs(tagsData) - - private fun ByCategoryArgs(): ByCategoryArgs = - ByCategoryArgs(tagsData) - - private fun SetArgs(): SetArgs = - SetArgs(tagsData) - - private fun EditArgs(): EditArgs = - EditArgs(tagsData) - - internal class ByKeyAndOptionalGuildArgs : Arguments() { - val key by string { - name = "key" - description = "Tag key to match by" - } - - val guild by optionalGuild { - name = "guild" - description = "Optional guild to match by - \"this\" for the current guild" - } - } - - internal class FindArgs : Arguments() { - val category by optionalString { - name = "category" - description = "Optional category to match by" - } - val key by optionalString { - name = "key" - description = "Optional tag key to match by" - } - - val guild by optionalGuild { - name = "guild" - description = "Optional guild to match by - \"this\" for the current guild" - } - } - - internal class SetArgs(tagsData: TagsData) : Arguments() { - val key by string { - name = "key" - description = "Unique tag key" - } - - val category by string { - name = "category" - description = "Category to use for this tag - specify a new one to create it" - - autoComplete { - val categories = tagsData.getAllCategories(data.guildId.value) - - suggestStringMap( - categories.associateWith { it }, - FilterStrategy.Contains - ) - } - } - - val colour by optionalColor { - name = "colour" - description = "Optional embed colour - use hex codes, RGB integers or Discord colour constants" - } - - val guild by optionalGuild { - name = "guild" - description = "Optional guild to limit the tag to - \"this\" for the current guild" - } - } - - internal class EditArgs(tagsData: TagsData) : Arguments() { - val key by string { - name = "key" - description = "Tag key to use for matching (this can't be edited)" - } - - val guild by optionalGuild { - name = "guild" - description = "Optional guild to use for matching (this can't be edited)" - } - - val category by optionalString { - name = "category" - description = "Category to use for this tag - specify a new one to create it" - - autoComplete { - val categories = tagsData.getAllCategories(data.guildId.value) - - suggestStringMap( - categories.associateWith { it }, - FilterStrategy.Contains - ) - } - } - - val colour by optionalColor { - name = "colour" - description = "Use hex codes, RGB integers (0 to clear) or Discord colour constants" - } - } - - internal class ByCategoryArgs(tagsData: TagsData) : Arguments() { - val category by string { - name = "category" - description = "Category to match by" - - autoComplete { - val categories = tagsData.getAllCategories(data.guildId.value) - - suggestStringMap( - categories.associateWith { it }, - FilterStrategy.Contains - ) - } - } - } - - internal class TagEditModal( - isEditing: Boolean = false, - key: String? = null, - private val initialTagTitle: String? = null, - private val initialDescription: String? = null, - private val initialImageUrl: String? = null, - ) : ModalForm() { - override var title: String = if (!isEditing) { - "Create tag" - } else { - "Edit tag" - } + if (key != null) { - ": $key" - } else { - "" - } - - val tagTitle = lineText { - label = "Title" - initialValue = initialTagTitle - } - - val description = paragraphText { - label = "Tag content" - initialValue = initialDescription - } - - val imageUrl = lineText { - label = "Image URL" - initialValue = initialImageUrl - - required = false - } - } - - internal class ByKeyArgs : Arguments() { - val key by string { - name = "key" - description = "Partial key to match by" - } - } - - internal class ByTitleArgs : Arguments() { - val title by string { - name = "title" - description = "Partial title to match by" - } - } - - internal class GetTagArgs(tagsData: TagsData) : Arguments() { - val tagKey by string { - name = "tag" - description = "Tag to retrieve" - - autoComplete { - val input = focusedOption.value - - var category: String? = null - lateinit var tagKey: String - - if ("/" in input) { - category = input.substringBeforeLast("/") - tagKey = input.substringAfterLast("/") - } else { - tagKey = input - } - - var potentialTags = ( - tagsData.getTagsByPartialKey(tagKey, this.data.guildId.value) + - tagsData.getTagsByPartialTitle(tagKey, this.data.guildId.value) - ) - .toSet() - .toList() - - if (category != null) { - potentialTags = potentialTags.filter { it.category.startsWith(category!!, true) } - } - - val foundKeys: MutableList = mutableListOf() - - potentialTags = potentialTags - .sortedBy { if (it.guildId == null) -1 else 1 } - .filter { - if (it.key !in foundKeys) { - foundKeys.add(it.key) - - true - } else { - false - } - } - - potentialTags = potentialTags - .sortedBy { it.title } - .take(25) - - suggestString { - potentialTags.forEach { - choice("(${it.category}/${it.key}) ${it.title}", it.key) - } - } - } - } - - val userToMention by optionalMember { - name = "user" - description = "User to mention along with this tag." - } - } - - // endregion -} diff --git a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/TagsPlugin.kt b/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/TagsPlugin.kt deleted file mode 100644 index 42d15798..00000000 --- a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/TagsPlugin.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.tags - -import com.kotlindiscord.kord.extensions.plugins.KordExPlugin -import com.kotlindiscord.kord.extensions.plugins.annotations.plugins.WiredPlugin -import org.pf4j.PluginWrapper - -/** - * Plugin containing the [TagsExtension], which removes pending users after they've lurked for a while. - */ -@WiredPlugin( - id = TagsPlugin.id, - version = "1.0.1-SNAPSHOT", - - author = "QuiltMC", - description = "Tags system, allowing for the addition and display of configurable text snippets.", - license = "Mozilla Public License 2.0" -) -public class TagsPlugin(wrapper: PluginWrapper) : KordExPlugin(wrapper) { - override suspend fun setup() { -// TODO: We can't really use the plugin system just yet since it doesn't currently support any configuration tooling -// extension(::TagsExtension) - } - - public companion object { - public const val id: String = "quiltmc-tags" - } -} diff --git a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/_Types.kt b/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/_Types.kt deleted file mode 100644 index fce09f79..00000000 --- a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/_Types.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -@file:Suppress("Filename") - -package org.quiltmc.community.cozy.modules.tags - -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import org.quiltmc.community.cozy.modules.tags.data.Tag - -/** Type alias representing a tag formatter callback. **/ -public typealias TagFormatter = suspend MessageCreateBuilder.(tag: Tag) -> Unit diff --git a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/_Utils.kt b/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/_Utils.kt deleted file mode 100644 index 92b144f8..00000000 --- a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/_Utils.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.tags - -import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder -import com.kotlindiscord.kord.extensions.utils.loadModule -import org.koin.dsl.bind -import org.quiltmc.community.cozy.modules.tags.config.SimpleTagsConfig -import org.quiltmc.community.cozy.modules.tags.config.TagsConfig -import org.quiltmc.community.cozy.modules.tags.data.TagsData -import org.quiltmc.community.cozy.modules.welcome.TagsExtension - -public fun ExtensibleBotBuilder.ExtensionsBuilder.tags(config: TagsConfig, data: TagsData) { - loadModule { single { config } bind TagsConfig::class } - loadModule { single { data } bind TagsData::class } - - add { TagsExtension() } -} - -public fun ExtensibleBotBuilder.ExtensionsBuilder.tags(data: TagsData, body: SimpleTagsConfig.Builder.() -> Unit) { - tags(SimpleTagsConfig(body), data) -} - -public fun String?.nullIfBlank(): String? = - if (isNullOrBlank()) { - null - } else { - this - } diff --git a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/config/SimpleTagsConfig.kt b/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/config/SimpleTagsConfig.kt deleted file mode 100644 index f6686eca..00000000 --- a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/config/SimpleTagsConfig.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.tags.config - -import com.kotlindiscord.kord.extensions.checks.types.Check -import dev.kord.core.entity.Guild -import dev.kord.core.entity.channel.GuildMessageChannel -import dev.kord.rest.builder.message.embed -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.lastOrNull -import org.quiltmc.community.cozy.modules.tags.TagFormatter - -/** - * A simple in-memory configuration class, useful if you don't need anything special for your config storage. - * - * Comes with a convenient builder, for easy configuration. - */ -public class SimpleTagsConfig(private val builder: Builder) : TagsConfig { - override suspend fun getTagFormatter(): TagFormatter = - builder.tagFormatter - - override suspend fun getUserCommandChecks(): List> = - builder.userCommandChecks - - override suspend fun getStaffCommandChecks(): List> = - builder.staffCommandChecks - - override suspend fun getLoggingChannelOrNull(guild: Guild): GuildMessageChannel? = - if (builder.loggingChannelName != null) { - guild.channels - .filterIsInstance() - .filter { channel -> channel.name.equals(builder.loggingChannelName, true) } - .lastOrNull() - } else { - null - } - - public class Builder { - public var tagFormatter: TagFormatter = { tag -> - embed { - title = tag.title - description = tag.description - color = tag.color - - footer { - text = "${tag.category}/${tag.key}" - } - - image = tag.image - } - } - - public var loggingChannelName: String? = null - - internal val userCommandChecks: MutableList> = mutableListOf() - internal val staffCommandChecks: MutableList> = mutableListOf() - - public fun userCommandCheck(body: Check<*>) { - userCommandChecks.add(body) - } - - public fun staffCommandCheck(body: Check<*>) { - staffCommandChecks.add(body) - } - } -} - -public fun SimpleTagsConfig(body: SimpleTagsConfig.Builder.() -> Unit): SimpleTagsConfig { - val builder = SimpleTagsConfig.Builder() - - body(builder) - - return SimpleTagsConfig(builder) -} diff --git a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/config/TagsConfig.kt b/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/config/TagsConfig.kt deleted file mode 100644 index 8f34c2a8..00000000 --- a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/config/TagsConfig.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.tags.config - -import com.kotlindiscord.kord.extensions.checks.types.Check -import dev.kord.core.entity.Guild -import dev.kord.core.entity.channel.GuildMessageChannel -import org.quiltmc.community.cozy.modules.tags.TagFormatter - -/** - * Interface representing the configuration for the tags module. Extend this and register an instance with Koin to - * change how the module is configured. - * - * All functions are suspending to allow for database access, for example, where needed. - */ -public interface TagsConfig { - /** - * Get the configured tag formatter callback, used to turn a tag into a message. **Users configuring this to avoid - * creating embeds should make sure to append to the message content instead of replacing it.** - */ - public suspend fun getTagFormatter(): TagFormatter - - /** - * Get the configured user command checks, used to ensure a user-facing command can be run. - */ - public suspend fun getUserCommandChecks(): List> - - /** - * Get the configured staff command checks, used to ensure a staff-facing command can be run. - */ - public suspend fun getStaffCommandChecks(): List> - - /** - * Get the logging channel for logging tag updates, returning `null` if this isn't needed. - */ - public suspend fun getLoggingChannelOrNull(guild: Guild): GuildMessageChannel? - - /** - * Function wrapping [getLoggingChannelOrNull], with a non-null assertion. - */ - public suspend fun getLoggingChannel(guild: Guild): GuildMessageChannel = - getLoggingChannelOrNull(guild)!! -} diff --git a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/data/MemoryTagsData.kt b/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/data/MemoryTagsData.kt deleted file mode 100644 index 4b45ff2d..00000000 --- a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/data/MemoryTagsData.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.tags.data - -import dev.kord.common.entity.Snowflake - -/** - * In-memory tags storage, intended for testing. Not optimised, doesn't store the tags when the bot stops. This class - * uses `startsWith` to match tags based on partial keys and titles. - * - * Use your own implementation for your bots. - */ -public class MemoryTagsData : TagsData { - private val tags: MutableList = mutableListOf() - - override suspend fun getTagByKey(key: String, guildId: Snowflake?): Tag? = - tags.firstOrNull { - it.key == key && (it.guildId == guildId || it.guildId == null) - } - - override suspend fun getTagsByCategory(category: String, guildId: Snowflake?): List = - tags.filter { - it.category == category && (it.guildId == guildId || it.guildId == null) - } - - override suspend fun getTagsByPartialKey(partialKey: String, guildId: Snowflake?): List = - tags.filter { - it.key.startsWith(partialKey, true) && (it.guildId == guildId || it.guildId == null) - } - - override suspend fun getTagsByPartialTitle(partialTitle: String, guildId: Snowflake?): List = - tags.filter { - it.title.startsWith(partialTitle, true) && (it.guildId == guildId || it.guildId == null) - } - - override suspend fun getAllCategories(guildId: Snowflake?): Set = - tags.filter { it.guildId == guildId || it.guildId == null } - .map { it.category }.toSet() - - override suspend fun findTags(category: String?, guildId: Snowflake?, key: String?): List = - tags.filter { - (category == null || it.category.equals(category, true)) && - (guildId == null || it.guildId == guildId) && - (key == null || it.key.equals(key, true)) - } - - override suspend fun setTag(tag: Tag) { - tags.removeIf { - it.key == tag.key && it.guildId == tag.guildId - } - - tags.add(tag) - } - - override suspend fun deleteTagByKey(key: String, guildId: Snowflake?): Tag? { - val tag = getTagByKey(key, guildId) - - tags.removeIf { - it.key == key && it.guildId == guildId - } - - return tag - } -} diff --git a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/data/Tag.kt b/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/data/Tag.kt deleted file mode 100644 index da30bd9f..00000000 --- a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/data/Tag.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.tags.data - -import dev.kord.common.Color -import dev.kord.common.entity.Snowflake -import kotlinx.serialization.Serializable - -/** - * Data class representing a single tag. Serializable, for flexible storage. - */ -@Serializable -public data class Tag( - val category: String, - val description: String, - val key: String, - val title: String, - - val color: Color? = null, - val guildId: Snowflake? = null, - val image: String? = null -) diff --git a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/data/TagsData.kt b/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/data/TagsData.kt deleted file mode 100644 index a21e3d04..00000000 --- a/module-tags/src/main/kotlin/org/quiltmc/community/cozy/modules/tags/data/TagsData.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.tags.data - -import dev.kord.common.entity.Snowflake - -/** - * Interface representing data storage for the tags extension. Extend this and implement the functions to create your - * own storage setup for your bot. - * - * For all functions that take a guild ID (except for the `delete` and `set` functions), you should also always match - * tags with a guild ID of `null`. These are global tags, and should be accessible regardless of the guild context. - * - * All functions are suspending to allow for database access, for example, where needed. - */ -public interface TagsData { - /** - * Get a tag by tag key and guild ID. - */ - public suspend fun getTagByKey(key: String, guildId: Snowflake? = null): Tag? - - /** - * Get a list of tags with the given category and guild ID. - */ - public suspend fun getTagsByCategory(category: String, guildId: Snowflake? = null): List - - /** - * Get a list of tags with the given partial key and guild ID. Should find tags with keys that partially match - * the given [partialKey], but it's up to you whether to use `contains` or `startsWith`. - */ - public suspend fun getTagsByPartialKey(partialKey: String, guildId: Snowflake? = null): List - - /** - * Get a list of tags with the given partial title and guild ID. Should find tags with titles that partially match - * the given [partialTitle], but it's up to you whether to use `contains` or `startsWith`. - */ - public suspend fun getTagsByPartialTitle(partialTitle: String, guildId: Snowflake? = null): List - - /** - * Get a set of all potential tag categories. - */ - public suspend fun getAllCategories(guildId: Snowflake? = null): Set - - /** - * Find tags using optionally-provided criteria. Ignore null values, so their criteria is always matched. - */ - public suspend fun findTags(category: String? = null, guildId: Snowflake? = null, key: String? = null): List - - /** - * Given a [Tag] object, store it (and persist it if needed), overwriting any tags with the same key and guild ID. - */ - public suspend fun setTag(tag: Tag) - - /** - * Convenience function wrapping [deleteTagByKey]. - */ - public suspend fun deleteTag(tag: Tag): Tag? = deleteTagByKey(tag.key, tag.guildId) - - /** - * Delete a tag by tag key and guild ID, if it exists. Return `null` if the tag didn't exist, otherwise return - * the removed tag. - */ - public suspend fun deleteTagByKey(key: String, guildId: Snowflake? = null): Tag? -} diff --git a/module-welcome/README.md b/module-welcome/README.md deleted file mode 100644 index b27e2eaa..00000000 --- a/module-welcome/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Welcome Channel Module - -This module allows you to easily maintain and update a bot-managed channel (for example, a welcome or rules channel) based on a YAML file that you change whenever you need to update it. It also supports dynamic blocks with content that can change over time, and will attempt to only update messages that have not changed. - -For more information on how to use this as a user, [please see this guide on Notion](https://gdude2002.notion.site/Welcome-Channel-Spec-01f5237b34454247882054e8e7f1dd80). diff --git a/module-welcome/build.gradle.kts b/module-welcome/build.gradle.kts deleted file mode 100644 index 6aa56f3b..00000000 --- a/module-welcome/build.gradle.kts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -plugins { - `api-module` - `cozy-module` - `published-module` -} - -dependencies { - detektPlugins(libs.detekt) - detektPlugins(libs.detekt.libraries) - - ksp(libs.kordex.annotationProcessor) - - implementation(libs.kordex.annotations) - implementation(libs.kordex.core) - - implementation(libs.kaml) - - implementation(libs.logging) - - implementation(platform(libs.kotlin.bom)) - implementation(libs.kotlin.stdlib) -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/WelcomeChannel.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/WelcomeChannel.kt deleted file mode 100644 index 565aef55..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/WelcomeChannel.kt +++ /dev/null @@ -1,280 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome - -import com.charleskorn.kaml.PolymorphismStyle -import com.charleskorn.kaml.Yaml -import com.charleskorn.kaml.YamlConfiguration -import com.charleskorn.kaml.YamlException -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.DiscordRelayedException -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent -import com.kotlindiscord.kord.extensions.utils.deleteIgnoringNotFound -import com.kotlindiscord.kord.extensions.utils.hasNotStatus -import com.kotlindiscord.kord.extensions.utils.scheduling.Scheduler -import com.kotlindiscord.kord.extensions.utils.scheduling.Task -import dev.kord.common.entity.MessageType -import dev.kord.common.entity.Snowflake -import dev.kord.core.behavior.channel.createMessage -import dev.kord.core.behavior.edit -import dev.kord.core.entity.Message -import dev.kord.core.entity.channel.GuildMessageChannel -import dev.kord.core.event.interaction.InteractionCreateEvent -import dev.kord.core.supplier.EntitySupplyStrategy -import dev.kord.rest.builder.message.allowedMentions -import dev.kord.rest.builder.message.create.UserMessageCreateBuilder -import dev.kord.rest.builder.message.embed -import dev.kord.rest.request.RestRequestException -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.http.* -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.toList -import kotlinx.serialization.decodeFromString -import org.koin.core.component.inject -import org.quiltmc.community.cozy.modules.welcome.blocks.Block -import org.quiltmc.community.cozy.modules.welcome.blocks.InteractionBlock -import org.quiltmc.community.cozy.modules.welcome.config.WelcomeChannelConfig -import kotlin.collections.set - -public class WelcomeChannel( - public val channel: GuildMessageChannel, - public val url: String, -) : KordExKoinComponent { - private var blocks: MutableList = mutableListOf() - - private val messageMapping: MutableMap = mutableMapOf() - - private val config: WelcomeChannelConfig by inject() - private val client = HttpClient() - - private lateinit var yaml: Yaml - private var task: Task? = null - - public val scheduler: Scheduler = Scheduler() - - public suspend fun handleInteraction(event: InteractionCreateEvent) { - blocks.forEach { - if (it is InteractionBlock) { - it.handleInteraction(event) - } - } - } - - public suspend fun setup() { - val taskDelay = config.getRefreshDelay() - - if (!::yaml.isInitialized) { - yaml = Yaml( - config.getSerializersModule(), - YamlConfiguration(polymorphismStyle = PolymorphismStyle.Property) - ) - } - - task?.cancel() - - if (taskDelay != null) { - task = scheduler.schedule(taskDelay, false) { - populate() - } - } - - populate() - - task?.start() - } - - public fun shutdown() { - task?.cancel() - scheduler.shutdown() - } - - private suspend fun fetchBlocks(): List { - try { - val response = client.get(url).body() - - return yaml.decodeFromString(response) - } catch (e: ClientRequestException) { - throw DiscordRelayedException("Failed to download the YAML file\n\n>>> $e") - } catch (e: YamlException) { - throw DiscordRelayedException("Failed to parse the given YAML\n\n>>> $e") - } - } - - public fun getBlocks(): List = - blocks.toList() - - public suspend fun populate() { - task?.cancel() - - val guild = channel.getGuild() - - @Suppress("TooGenericExceptionCaught") - try { - blocks = fetchBlocks().toMutableList() - } catch (e: Exception) { - log { - embed { - title = "Welcome channel update failed" - color = DISCORD_RED - - description = buildString { - appendLine("**__Failed to update blocks__**") - appendLine() - appendLine("```") - appendLine(e) - appendLine("```") - } - - field { - name = "Channel" - value = "${channel.mention} (`${channel.id}` / `${channel.name}`)" - } - } - } - - throw e - } - - blocks.forEach { - it.channel = channel - it.guild = guild - } - - val messages = channel.withStrategy(EntitySupplyStrategy.rest) - .messages - .filter { it.author?.id == channel.kord.selfId } - .filter { it.type == MessageType.Default } - .toList() - .sortedBy { it.id.timestamp } - - @Suppress("TooGenericExceptionCaught") - try { - if (messages.size > blocks.size) { - messages.forEachIndexed { index, message -> - val block = blocks.getOrNull(index) - - if (block != null) { - if (messageNeedsUpdate(message, block)) { - message.edit { - block.edit(this) - - allowedMentions { } - } - } - - messageMapping[message.id] = block - } else { - message.delete() - messageMapping.remove(message.id) - } - } - } else { - blocks.forEachIndexed { index, block -> - val message = messages.getOrNull(index) - - if (message != null) { - if (messageNeedsUpdate(message, block)) { - message.edit { - block.edit(this) - - allowedMentions { } - } - } - - messageMapping[message.id] = block - } else { - val newMessage = channel.createMessage { - block.create(this) - - allowedMentions { } - } - - messageMapping[newMessage.id] = block - } - } - } - } catch (e: Exception) { - log { - embed { - title = "Welcome channel update failed" - color = DISCORD_RED - - description = buildString { - appendLine("**__Failed to update messages__**") - appendLine() - appendLine("```") - appendLine(e) - appendLine("```") - } - - field { - name = "Channel" - value = "${channel.mention} (`${channel.id}` / `${channel.name}`)" - } - } - } - - throw e - } - - task?.start() - } - - public suspend fun log(builder: suspend UserMessageCreateBuilder.() -> Unit): Message? = - config.getLoggingChannel(channel, channel.guild.asGuild())?.createMessage { builder() } - - public suspend fun clear() { - val messages = channel.withStrategy(EntitySupplyStrategy.rest) - .messages - .toList() - .filter { it.type == MessageType.Default } - - try { - channel.bulkDelete(messages.map { it.id }) - } catch (e: RestRequestException) { - if (e.hasNotStatus(HttpStatusCode.NotFound)) { - @Suppress("TooGenericExceptionCaught") - try { - messages.forEach { it.deleteIgnoringNotFound() } - } catch (e: Exception) { - log { - embed { - title = "Failed to clear welcome channel" - color = DISCORD_RED - - description = buildString { - appendLine("**__Failed to clear channel__**") - appendLine() - appendLine("```") - appendLine(e) - appendLine("```") - } - - field { - name = "Channel" - value = "${channel.mention} (`${channel.id}` / `${channel.name}`)" - } - } - } - - throw e - } - } - } - } - - private suspend fun messageNeedsUpdate(message: Message, block: Block): Boolean { - val builder = UserMessageCreateBuilder() - - block.create(builder) - - return !builder.isSimilar(message) - } -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/WelcomeExtension.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/WelcomeExtension.kt deleted file mode 100644 index a223f236..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/WelcomeExtension.kt +++ /dev/null @@ -1,372 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome - -import com.kotlindiscord.kord.extensions.DISCORD_YELLOW -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.channel -import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBoolean -import com.kotlindiscord.kord.extensions.commands.converters.impl.string -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.extensions.event -import dev.kord.common.entity.Snowflake -import dev.kord.core.behavior.channel.asChannelOf -import dev.kord.core.behavior.channel.asChannelOfOrNull -import dev.kord.core.behavior.channel.createMessage -import dev.kord.core.entity.Message -import dev.kord.core.entity.channel.GuildMessageChannel -import dev.kord.core.event.guild.GuildCreateEvent -import dev.kord.core.event.interaction.InteractionCreateEvent -import dev.kord.rest.builder.message.create.UserMessageCreateBuilder -import dev.kord.rest.builder.message.embed -import kotlinx.coroutines.flow.toList -import org.koin.core.component.inject -import org.quiltmc.community.cozy.modules.welcome.config.WelcomeChannelConfig -import org.quiltmc.community.cozy.modules.welcome.data.WelcomeChannelData - -/****/ -public class WelcomeExtension : Extension() { - override val name: String = "quiltmc-welcome" - - private val config: WelcomeChannelConfig by inject() - private val data: WelcomeChannelData by inject() - - private val welcomeChannels: MutableMap = mutableMapOf() - - override suspend fun setup() { - val initialMapping = data.getChannelURLs() - - event { - action { - for (it in event.guild.channels.toList()) { - val channel = it.asChannelOfOrNull() - ?: continue - - val url = initialMapping[channel.id] - ?: continue - - val welcomeChannel = WelcomeChannel(channel, url) - - welcomeChannels[channel.id] = welcomeChannel - - welcomeChannel.setup() - } - } - } - - event { - action { - welcomeChannels[event.interaction.channelId]?.handleInteraction(event) - } - } - - ephemeralSlashCommand { - name = "welcome-channels" - description = "Manage welcome channels" - - allowInDms = false - - config.getStaffCommandChecks().forEach(::check) - - ephemeralSlashCommand(::ChannelArgs) { - name = "blocks" - description = "Get a list of the configured blocks" - - action { - val welcomeChannel = welcomeChannels[arguments.channel.id] - - if (welcomeChannel == null) { - respond { - content = "No configuration for ${arguments.channel.mention} exists" - } - - return@action - } - - val blocks = welcomeChannel.getBlocks() - - respond { - content = buildString { - if (blocks.isEmpty()) { - append("A configuration was found, but it doesn't contain any blocks.") - } else { - blocks.forEach { - appendLine("**»** ${it.javaClass.simpleName}") - } - } - } - } - } - } - - ephemeralSubCommand(::ChannelArgs) { - name = "delete" - description = "Delete a welcome channel configuration" - - action { - val welcomeChannel = welcomeChannels[arguments.channel.id] - - if (welcomeChannel != null) { - welcomeChannel.shutdown() - welcomeChannels.remove(arguments.channel.id) - - val deletedUrl = data.removeChannel(arguments.channel.id) - - respond { - content = "Configuration removed - old URL was `$deletedUrl`" - } - - welcomeChannel.log { - embed { - title = "Welcome channel removed" - color = DISCORD_YELLOW - - description = "Welcome channel configuration removed." - - field { - name = "Channel" - value = "${welcomeChannel.channel.mention} (" + - "`${welcomeChannel.channel.id}` / " + - "`${welcomeChannel.channel.name}`" + - ")" - } - - field { - name = "Staff Member" - value = "${user.mention} (" + - "`${user.id}` / " + - "`${user.asUser().tag}`" + - ")" - } - } - } - } else { - respond { - content = "No configuration for ${arguments.channel.mention} exists" - } - } - } - } - - ephemeralSubCommand(::ChannelArgs) { - name = "get" - description = "Get the url for a welcome channel, if it's configured" - - action { - val url = data.getUrlForChannel(arguments.channel.id) - - respond { - content = if (url != null) { - "The configuration URL for ${arguments.channel.mention} is `$url`" - } else { - "No configuration for ${arguments.channel.mention} exists" - } - } - } - } - - ephemeralSubCommand(::ChannelRefreshArgs) { - name = "refresh" - description = "Manually repopulate the given welcome channel" - - action { - val welcomeChannel = welcomeChannels[arguments.channel.id] - - if (welcomeChannel == null) { - respond { - content = "No configuration for ${arguments.channel.mention} exists" - } - - return@action - } - - respond { - content = "Manually refreshing ${arguments.channel.mention} now..." - } - welcomeChannel.log { - embed { - title = "Welcome channel refreshed" - color = DISCORD_YELLOW - - description = buildString { - append("Manually ") - - if (arguments.clear) { - append("**clearing** and ") - } - - append("refreshing welcome channel...") - } - - field { - name = "Channel" - value = "${welcomeChannel.channel.mention} (" + - "`${welcomeChannel.channel.id}` / " + - "`${welcomeChannel.channel.name}`" + - ")" - } - - field { - name = "Staff Member" - value = "${user.mention} (" + - "`${user.id}` / " + - "`${user.asUser().tag}`" + - ")" - } - } - } - - if (arguments.clear) { - welcomeChannel.clear() - } - - welcomeChannel.populate() - } - } - - ephemeralSubCommand(::ChannelCreateArgs) { - name = "set" - description = "Set the URL for a welcome channel, and populate it" - - action { - var welcomeChannel = welcomeChannels[arguments.channel.id] - - if (welcomeChannel != null) { - welcomeChannels.remove(arguments.channel.id) - welcomeChannel.shutdown() - } - - welcomeChannel = WelcomeChannel(arguments.channel.asChannelOf(), arguments.url) - - data.setUrlForChannel(arguments.channel.id, arguments.url) - welcomeChannels[arguments.channel.id] = welcomeChannel - - respond { - content = buildString { - append("Set the configuration URL for ${arguments.channel.mention} to `${arguments.url}`, ") - - if (arguments.clear) { - append("clearing and ") - } - - append("refreshing...") - } - } - - welcomeChannel.log { - embed { - title = "Welcome channel created/edited" - color = DISCORD_YELLOW - - description = buildString { - append("Welcome channel URL set: `${arguments.url}`") - - if (arguments.clear) { - appendLine() - appendLine("**Clearing channel...**") - } - } - - field { - name = "Channel" - value = "${welcomeChannel.channel.mention} (" + - "`${welcomeChannel.channel.id}` / " + - "`${welcomeChannel.channel.name}`" + - ")" - } - - field { - name = "Staff Member" - value = "${user.mention} (" + - "`${user.id}` / " + - "`${user.asUser().tag}`" + - ")" - } - } - } - - if (arguments.clear) { - welcomeChannel.clear() - } - - welcomeChannel.setup() - } - } - } - } - - public suspend fun log(channel: GuildMessageChannel, builder: UserMessageCreateBuilder.() -> Unit): Message? = - config.getLoggingChannel(channel, channel.guild.asGuild())?.createMessage { builder() } - - override suspend fun unload() { - welcomeChannels.values.forEach { it.shutdown() } - welcomeChannels.clear() - } - - internal class ChannelCreateArgs : Arguments() { - val channel by channel { - name = "channel" - description = "Channel representing a welcome channel" - } - - val url by string { - name = "url" - description = "Public link to a YAML file used to configure a welcome channel" - - validate { - failIf("URLs must contain a protocol (eg `https://`)") { - value.contains("://").not() || - value.startsWith("://") - } - } - } - - val clear by defaultingBoolean { - name = "clear" - description = "Whether to clear the channel before repopulating it" - defaultValue = false - } - } - - internal class ChannelRefreshArgs : Arguments() { - val channel by channel { - name = "channel" - description = "Channel representing a welcome channel" - - validate { - failIf("Given channel must be a message channel on the current server") { - val guildChannel = value.asChannelOfOrNull() - - guildChannel == null || guildChannel.guildId != context.getGuild()?.id - } - } - } - - val clear by defaultingBoolean { - name = "clear" - description = "Whether to clear the channel before repopulating it" - defaultValue = false - } - } - - internal class ChannelArgs : Arguments() { - val channel by channel { - name = "channel" - description = "Channel representing a welcome channel" - - validate { - failIf("Given channel must be a message channel on the current server") { - val guildChannel = value.asChannelOfOrNull() - - guildChannel == null || guildChannel.guildId != context.getGuild()?.id - } - } - } - } -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/_Utils.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/_Utils.kt deleted file mode 100644 index 6d97f32d..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/_Utils.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -@file:Suppress("DEPRECATION") - -package org.quiltmc.community.cozy.modules.welcome - -import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder -import com.kotlindiscord.kord.extensions.utils.loadModule -import dev.kord.common.entity.DiscordComponent -import dev.kord.common.entity.EmbedType -import dev.kord.core.entity.Message -import dev.kord.core.entity.channel.Channel -import dev.kord.core.entity.component.Component -import dev.kord.rest.builder.message.EmbedBuilder -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import org.koin.dsl.bind -import org.quiltmc.community.cozy.modules.welcome.config.SimpleWelcomeChannelConfig -import org.quiltmc.community.cozy.modules.welcome.config.WelcomeChannelConfig -import org.quiltmc.community.cozy.modules.welcome.data.WelcomeChannelData - -private const val DISCORD_CHANNEL_URI = "https://discord.com/channels" - -public fun ExtensibleBotBuilder.ExtensionsBuilder.welcomeChannel( - config: WelcomeChannelConfig, - data: WelcomeChannelData -) { - loadModule { single { config } bind WelcomeChannelConfig::class } - loadModule { single { data } bind WelcomeChannelData::class } - - add { WelcomeExtension() } -} - -public fun ExtensibleBotBuilder.ExtensionsBuilder.welcomeChannel( - data: WelcomeChannelData, - body: SimpleWelcomeChannelConfig.Builder.() -> Unit -) { - welcomeChannel(SimpleWelcomeChannelConfig(body), data) -} - -public inline fun List.ifNotEmpty(body: (Collection).() -> List): List { - if (this.isNotEmpty()) { - return body() - } - - return emptyList() -} - -public fun MessageCreateBuilder.isSimilar(other: Message): Boolean { - val builderComponents = components - ?.mapNotNull { it.build().components.value } - ?.ifNotEmpty { - reduce { left, right -> left + right } - } ?: emptyList() - - val messageComponents = other.actionRows - .map { it.components } - .ifNotEmpty { - reduce { left, right -> left + right } - } - - val messageEmbedBuilders = other.embeds - .filter { it.type == null || it.type == EmbedType.Rich } - .map { embed -> - EmbedBuilder().also { - embed.apply(it) - } - } - - if (content == null) { - content = "" - } - - return content == other.content && - embeds?.size == messageEmbedBuilders.size && - componentsAreSimilar(builderComponents, messageComponents) && - - embeds?.filterIndexed { index, embed -> - val otherEmbed = messageEmbedBuilders[index] - - embed.isSimilar(otherEmbed) - }?.size == embeds?.size -} - -public fun componentsAreSimilar( - builderComponents: List, - messageComponents: List -): Boolean { - if (builderComponents.size != messageComponents.size) { - return false - } - - if (builderComponents.isEmpty()) { - return true - } - - val results: MutableList = mutableListOf() - - builderComponents.forEachIndexed { index, builderComponent -> - val messageComponent = messageComponents[index] - - results.add( - builderComponent.customId == messageComponent.data.customId && - builderComponent.type == messageComponent.type && - builderComponent.label == messageComponent.data.label && - builderComponent.emoji == messageComponent.data.emoji && - builderComponent.disabled == messageComponent.data.disabled && - builderComponent.url == messageComponent.data.url - ) - } - - return results.all { it } -} - -public fun EmbedBuilder.isSimilar(other: EmbedBuilder): Boolean { - return title?.trim() == other.title?.trim() && - description?.trim() == other.description?.trim() && - footer?.text?.trim() == other.footer?.text?.trim() && - footer?.icon?.trim() == other.footer?.icon?.trim() && - image?.trim() == other.image?.trim() && - thumbnail?.url?.trim() == other.thumbnail?.url?.trim() && - author?.icon?.trim() == other.author?.icon?.trim() && - author?.url?.trim() == other.author?.url?.trim() && - author?.name?.trim() == other.author?.name?.trim() && - - color == other.color && - timestamp == other.timestamp && - - fields.all { field -> - other.fields.any { otherField -> - field.inline == otherField.inline && - field.value.trim() == otherField.value.trim() && - field.name.trim() == otherField.name.trim() - } - } -} - -/** - * Generate the jump URL for this channel. - * - * @return A clickable URL to jump to this channel. - */ -public fun Channel.getJumpUrl(): String = - "$DISCORD_CHANNEL_URI/${data.guildId.value?.value ?: "@me"}/${id.value}" diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/Block.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/Block.kt deleted file mode 100644 index 32265d74..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/Block.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import dev.kord.core.entity.Guild -import dev.kord.core.entity.channel.GuildMessageChannel -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.modify.MessageModifyBuilder -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient - -@Suppress("UnnecessaryAbstractClass") -@Serializable -public abstract class Block { - @Transient - public lateinit var channel: GuildMessageChannel - - @Transient - public lateinit var guild: Guild - - public abstract suspend fun create(builder: MessageCreateBuilder) - public abstract suspend fun edit(builder: MessageModifyBuilder) -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/ComplianceBlock.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/ComplianceBlock.kt deleted file mode 100644 index 192828d4..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/ComplianceBlock.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -@file:Suppress("DataClassContainsFunctions") - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent -import dev.kord.common.entity.ButtonStyle -import dev.kord.common.entity.Snowflake -import dev.kord.core.Kord -import dev.kord.core.behavior.channel.createEmbed -import dev.kord.core.behavior.getChannelOfOrNull -import dev.kord.core.behavior.interaction.response.respond -import dev.kord.core.entity.Member -import dev.kord.core.entity.Role -import dev.kord.core.entity.channel.TextChannel -import dev.kord.core.event.interaction.ButtonInteractionCreateEvent -import dev.kord.core.event.interaction.InteractionCreateEvent -import dev.kord.rest.builder.message.actionRow -import dev.kord.rest.builder.message.allowedMentions -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.modify.MessageModifyBuilder -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.datetime.Clock -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.koin.core.component.inject - -@Suppress("MagicNumber") -@Serializable -@SerialName("compliance") -public data class ComplianceBlock( - val id: String, - val role: Snowflake, - val title: String, - val buttonText: String, - val logChannel: Snowflake, - val complianceTypeUser: String, - - val template: String = "__**{TITLE}**__\n\n" + - - "By clicking the button below, you certify that:\n\n" + - - "**»** {COMPLIANCE_TYPE_USER}\n\n" + - - "**Please note:** Once you certify the above, you can't revoke your certification. If you decide to " + - "certify the above when it's not true, you may be punished or removed from the server.", - - val complianceTypeLogs: String = complianceTypeUser - .replace("You", "They") - .replace("you", "they") -) : Block(), InteractionBlock, KordExKoinComponent { - val kord: Kord by inject() - - private suspend fun getGuildRole(): Role? = - guild.roles.firstOrNull { it.id == role } - - private fun generateButtonId(): String = - "compliance/button/${channel.id}/$id" - - private suspend fun handleButton(event: ButtonInteractionCreateEvent) { - if (event.interaction.componentId != generateButtonId()) { - return - } - - val response = event.interaction.deferEphemeralResponse() - val role = getGuildRole() - - if (role == null) { - response.respond { - content = "**Error:** The configured role doesn't seem to exist.\n\n" + - - "Please report this to the server's staff team." - } - - return - } - - val member = event.interaction.user.asMember(guild.id) - - if (role.id in member.roleIds) { - response.respond { - content = "**Error:** You've already certified this; you can't do so again." - } - } - - val logged = logCertification(member) - - if (!logged) { - response.respond { - content = "**Error:** The configured compliance logging channel doesn't seem to exist, or it's not " + - "a text channel.\n\n" + - - "Please report this to the server's staff team." - } - - return - } - - member.addRole(role.id) - - response.respond { - content = "Your certification has been recorded, and the <@&${role.id}> role has been granted. Thanks!" - } - } - - private suspend fun logCertification(user: Member): Boolean { - val channel = guild.getChannelOfOrNull(logChannel) - ?: return false - - channel.createEmbed { - title = "Compliance Logging" - color = DISCORD_BLURPLE - timestamp = Clock.System.now() - - description = "${user.mention} has certified that:\n\n" + - - "**»** $complianceTypeLogs" - - field { - inline = true - name = "Block ID" - - value = id - } - - field { - inline = true - name = "User" - - value = "${user.mention} (`${user.tag}` / `${user.id}`)" - } - } - - return true - } - - private fun getMessageText(): String = template - .replace("{COMPLIANCE_TYPE_USER}", complianceTypeUser) - .replace("{ROLE_ID}", role.toString()) - .replace("{ROLE_MENTION}", "<@&$role>") - .replace("{TITLE}", title) - - override suspend fun create(builder: MessageCreateBuilder) { - builder.content = getMessageText() - builder.allowedMentions { } - - builder.actionRow { - interactionButton(ButtonStyle.Primary, generateButtonId()) { - label = buttonText - } - } - } - - override suspend fun edit(builder: MessageModifyBuilder) { - builder.content = getMessageText() - builder.allowedMentions { } - - builder.actionRow { - interactionButton(ButtonStyle.Primary, generateButtonId()) { - label = buttonText - } - } - } - - override suspend fun handleInteraction(event: InteractionCreateEvent) { - if (event is ButtonInteractionCreateEvent) { - handleButton(event) - } - } -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/EmbedBlock.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/EmbedBlock.kt deleted file mode 100644 index dbb7654f..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/EmbedBlock.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent -import dev.kord.core.Kord -import dev.kord.core.cache.data.EmbedData -import dev.kord.core.entity.Embed -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.embed -import dev.kord.rest.builder.message.modify.MessageModifyBuilder -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.koin.core.component.inject - -@Suppress("MagicNumber") -@Serializable -@SerialName("embed") -public data class EmbedBlock( - val embeds: List, - val text: String? = null -) : Block(), KordExKoinComponent { - val kord: Kord by inject() - - init { - if (embeds.isEmpty() || embeds.size > 10) { - error("Must provide up to 10 embeds") - } - } - - override suspend fun create(builder: MessageCreateBuilder) { - builder.content = text - - embeds.forEach { embed -> - builder.embed { - Embed(embed, kord).apply(this) - } - } - } - - override suspend fun edit(builder: MessageModifyBuilder) { - builder.content = text - builder.components = mutableListOf() - - embeds.forEach { embed -> - builder.embed { - Embed(embed, kord).apply(this) - } - } - } -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/InteractionBlock.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/InteractionBlock.kt deleted file mode 100644 index 74fd1c20..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/InteractionBlock.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import dev.kord.core.event.interaction.InteractionCreateEvent - -public interface InteractionBlock { - public suspend fun handleInteraction(event: InteractionCreateEvent) -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/LinksBlock.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/LinksBlock.kt deleted file mode 100644 index dd46049b..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/LinksBlock.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE -import dev.kord.common.Color -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.embed -import dev.kord.rest.builder.message.modify.MessageModifyBuilder -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Suppress("DataClassContainsFunctions") -@Serializable -@SerialName("links") -public data class LinksBlock( - val title: String, - val links: Map, - val text: String? = null, - val color: Color = DISCORD_BLURPLE, - val description: String? = null, - val template: String = "**»** [{TEXT}]({URL})" -) : Block() { - init { - if (links.isEmpty()) { - error("Must provide at least one link") - } - } - - private fun buildDescription() = buildString { - if (description != null) { - append(description) - - appendLine() - appendLine() - } - - links.forEach { (text, url) -> - appendLine( - template - .replace("{TEXT}", text) - .replace("{URL}", url) - ) - } - } - - override suspend fun create(builder: MessageCreateBuilder) { - builder.content = text - - builder.embed { - title = this@LinksBlock.title - color = this@LinksBlock.color - - description = buildDescription() - } - } - - override suspend fun edit(builder: MessageModifyBuilder) { - builder.content = text - builder.components = mutableListOf() - - builder.embed { - title = this@LinksBlock.title - color = this@LinksBlock.color - - description = buildDescription() - } - } -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/MessageCopyBlock.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/MessageCopyBlock.kt deleted file mode 100644 index d85bb783..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/MessageCopyBlock.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent -import dev.kord.common.Color -import dev.kord.common.entity.Snowflake -import dev.kord.core.Kord -import dev.kord.core.behavior.getChannelOfOrNull -import dev.kord.core.entity.Message -import dev.kord.core.entity.channel.GuildMessageChannel -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.embed -import dev.kord.rest.builder.message.modify.MessageModifyBuilder -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.koin.core.component.inject - -@Suppress("MagicNumber") -@Serializable -@SerialName("message_copy") -public data class MessageCopyBlock( - @SerialName("message_url") - val messageUrl: String, - - val color: Color = DISCORD_BLURPLE, - val template: String = "{TEXT}", - val title: String? = null, - - @SerialName("use_embed") - val useEmbed: Boolean = false, -) : Block(), KordExKoinComponent { - val kord: Kord by inject() - - init { - if ("{TEXT}" !in template) { - error("Must provide a {TEXT} placeholder in the template") - } - } - - override suspend fun create(builder: MessageCreateBuilder) { - val message = retrieveMessage(messageUrl) - - val content = template.replace("{TEXT}", message.content) - - if (useEmbed) { - builder.embed { - this@embed.color = this@MessageCopyBlock.color - this@embed.description = content - - if (!this@MessageCopyBlock.title.isNullOrBlank()) { - this@embed.title = this@MessageCopyBlock.title - } - } - } else { - builder.content = content - } - } - - override suspend fun edit(builder: MessageModifyBuilder) { - val message = retrieveMessage(messageUrl) - - val content = template.replace("{TEXT}", message.content) - - if (useEmbed) { - builder.embed { - this@embed.color = this@MessageCopyBlock.color - this@embed.description = content - - if (!this@MessageCopyBlock.title.isNullOrBlank()) { - this@embed.title = this@MessageCopyBlock.title - } - } - } else { - builder.content = content - } - } -} - -@Suppress("MagicNumber") -public suspend fun MessageCopyBlock.retrieveMessage(url: String): Message { - val ids = url.substringAfter("channels/") - .split("/") - .map { Snowflake(it) } - - val message = kord.getGuildOrNull(ids[0]) - ?.getChannelOfOrNull(ids[1]) - ?.getMessageOrNull(ids[2]) - ?: error("Unable to get message at URL: $messageUrl") - - if (message.getGuild().id != guild.id) { - error("Message is not from the current server: $messageUrl") - } - - return message -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/RolesBlock.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/RolesBlock.kt deleted file mode 100644 index a9b82b1e..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/RolesBlock.kt +++ /dev/null @@ -1,207 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -@file:Suppress("DataClassContainsFunctions") - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent -import com.kotlindiscord.kord.extensions.utils.emoji -import com.kotlindiscord.kord.extensions.utils.toReaction -import dev.kord.common.Color -import dev.kord.common.entity.ButtonStyle -import dev.kord.common.entity.Snowflake -import dev.kord.core.Kord -import dev.kord.core.behavior.edit -import dev.kord.core.behavior.interaction.response.respond -import dev.kord.core.entity.Role -import dev.kord.core.event.interaction.ButtonInteractionCreateEvent -import dev.kord.core.event.interaction.InteractionCreateEvent -import dev.kord.core.event.interaction.SelectMenuInteractionCreateEvent -import dev.kord.rest.builder.component.option -import dev.kord.rest.builder.message.EmbedBuilder -import dev.kord.rest.builder.message.actionRow -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.embed -import dev.kord.rest.builder.message.modify.MessageModifyBuilder -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.toList -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.koin.core.component.inject - -@Suppress("MagicNumber") -@Serializable -@SerialName("roles") -public data class RolesBlock( - val id: String, - val roles: Map, - val title: String = "Role Assignment", - val description: String = "Click the button below to assign yourself any of the following roles.", - val color: Color = DISCORD_BLURPLE, - val template: String = "**»** {MENTION} {DESCRIPTION}" -) : Block(), InteractionBlock, KordExKoinComponent { - val kord: Kord by inject() - - init { - if (roles.isEmpty() || roles.size > 25) { - error("Must provide up to 25 roles") - } - } - - private suspend fun EmbedBuilder.setUp() { - val guildRoles = getGuildRoles() - - title = this@RolesBlock.title - color = this@RolesBlock.color - - description = buildString { - append(this@RolesBlock.description) - - appendLine() - appendLine() - - guildRoles.forEach { (id, role) -> - val roleItem = this@RolesBlock.roles[id]!! - - append( - template - .replace("{MENTION}", role.mention) - .replace("{NAME}", role.name) - .replace("{ID}", role.id.toString()) - .replace("{DESCRIPTION}", roleItem.description) - .replace("{EMOJI}", roleItem.emoji ?: "❓") - ) - - appendLine() - } - } - } - - private suspend fun getGuildRoles(): Map { - // Each role ID in the order they appear in the YAML - val sortedRoleIds = roles.toList().map { it.first } - - return guild.roles - .filter { it.id in roles.keys } - .toList() - .sortedBy { sortedRoleIds.indexOf(it.id) } - .associateBy { it.id } - } - - private fun generateButtonId(): String = - "roles/button/${channel.id}/$id" - - private fun generateMenuId(): String = - "roles/menu/${channel.id}/$id" - - private suspend fun handleButton(event: ButtonInteractionCreateEvent) { - if (event.interaction.componentId != generateButtonId()) { - return - } - - val response = event.interaction.deferEphemeralResponse() - - val guildRoles = getGuildRoles() - val userRoles = event.interaction.user.asMember(guild.id).roleIds - - response.respond { - content = "Please select your roles using the menu below." - - actionRow { - stringSelect(generateMenuId()) { - guildRoles.forEach { (id, role) -> - val emojiString = roles[id]!!.emoji - - option("@${role.name}", id.toString()) { - default = id in userRoles - - if (emojiString != null) { - emoji(emojiString.toReaction()) - } - } - } - - allowedValues = 0..guildRoles.size - } - } - } - } - - private suspend fun handleMenu(event: SelectMenuInteractionCreateEvent) { - if (event.interaction.componentId != generateMenuId()) { - return - } - - val response = event.interaction.deferEphemeralResponse() - - val guildRoles = getGuildRoles() - val member = event.interaction.user.asMember(guild.id) - val userRoles = member.roleIds.filter { it in guildRoles.keys } - - val selectedRoles = event.interaction.values - .map { Snowflake(it) } - .filter { it in guildRoles.keys } - - val toAdd = selectedRoles.filterNot { it in userRoles } - val toRemove = userRoles.filterNot { it in selectedRoles } - - if (toAdd.isEmpty() && toRemove.isEmpty()) { - response.respond { - content = "It looks like you picked all the same roles, so no changes have been made." - } - - return - } - - member.edit { - roles = member.roleIds.toMutableSet() - - roles!!.addAll(toAdd) - roles!!.removeAll(toRemove) - } - - response.respond { - content = "Your roles have been updated!" - } - } - - override suspend fun create(builder: MessageCreateBuilder) { - builder.embed { setUp() } - - builder.actionRow { - interactionButton(ButtonStyle.Primary, generateButtonId()) { - label = "Pick roles" - } - } - } - - override suspend fun edit(builder: MessageModifyBuilder) { - builder.embed { setUp() } - - builder.actionRow { - interactionButton(ButtonStyle.Primary, generateButtonId()) { - label = "Pick roles" - } - } - } - - override suspend fun handleInteraction(event: InteractionCreateEvent) { - when (event) { - is ButtonInteractionCreateEvent -> handleButton(event) - is SelectMenuInteractionCreateEvent -> handleMenu(event) - - else -> return - } - } -} - -@Serializable -public data class RoleItem( - val description: String, - val emoji: String? = null -) diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/RulesBlock.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/RulesBlock.kt deleted file mode 100644 index ea863c27..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/RulesBlock.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import dev.kord.common.Color -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.embed -import dev.kord.rest.builder.message.modify.MessageModifyBuilder -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Suppress("MagicNumber") -@Serializable -@SerialName("rules") -public data class RulesBlock( - val rules: LinkedHashMap, - val text: String? = null, - val startingIndex: Int = 1, - - val colors: List = listOf( - Color(0xff0000), - Color(0xff8c00), - Color(0xe1ff00), - Color(0x55ff00), - Color(0x00ff37), - Color(0x00ffc8), - Color(0x00aaff), - Color(0x001eff), - Color(0x7300ff), - Color(0xff00ff), - ) -) : Block() { - init { - if (rules.isEmpty() || rules.size > 10) { - error("Must provide up to 10 rules") - } - - if (colors.size < rules.size) { - error("${rules.size} rules were provided, but not enough colours (${colors.size})") - } - } - - override suspend fun create(builder: MessageCreateBuilder) { - builder.content = text - - var currentIndex = 0 - var humanIndex = currentIndex + startingIndex - - rules.forEach { (rule, text) -> - builder.embed { - title = "$humanIndex. $rule" - description = text - - color = colors[currentIndex] - } - - currentIndex += 1 - humanIndex = currentIndex + startingIndex - } - } - - override suspend fun edit(builder: MessageModifyBuilder) { - builder.content = text - builder.components = mutableListOf() - - var currentIndex = 0 - var humanIndex = currentIndex + startingIndex - - rules.forEach { (rule, text) -> - builder.embed { - title = "$humanIndex. $rule" - description = text - - color = colors[currentIndex] - } - - currentIndex += 1 - humanIndex = currentIndex + startingIndex - } - } -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/TextBlock.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/TextBlock.kt deleted file mode 100644 index d149e5cf..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/TextBlock.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.modify.MessageModifyBuilder -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -@SerialName("text") -public data class TextBlock( - val text: String -) : Block() { - override suspend fun create(builder: MessageCreateBuilder) { - builder.content = text - } - - override suspend fun edit(builder: MessageModifyBuilder) { - builder.content = text - builder.embeds = mutableListOf() - builder.components = mutableListOf() - } -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/ThreadListBlock.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/ThreadListBlock.kt deleted file mode 100644 index 34fedc42..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/blocks/ThreadListBlock.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.blocks - -import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE -import com.kotlindiscord.kord.extensions.time.TimestampType -import com.kotlindiscord.kord.extensions.time.toDiscord -import dev.kord.common.Color -import dev.kord.common.entity.ChannelType -import dev.kord.common.entity.Permission -import dev.kord.core.behavior.channel.asChannelOfOrNull -import dev.kord.core.entity.channel.TextChannel -import dev.kord.core.entity.channel.thread.ThreadChannel -import dev.kord.rest.builder.message.EmbedBuilder -import dev.kord.rest.builder.message.create.MessageCreateBuilder -import dev.kord.rest.builder.message.embed -import dev.kord.rest.builder.message.modify.MessageModifyBuilder -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.toList -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.quiltmc.community.cozy.modules.welcome.enums.ThreadListType -import org.quiltmc.community.cozy.modules.welcome.getJumpUrl - -@Suppress("DataClassContainsFunctions") -@Serializable -@SerialName("threads") -public data class ThreadListBlock( - val mode: ThreadListType, - val limit: Int = 10, - - val text: String? = null, - val description: String? = null, - val color: Color = DISCORD_BLURPLE, - val title: String = "${mode.humanReadable} Threads", - val template: String = "**»** [{NAME}]({URL})", - - @SerialName("active_emoji") - val activeEmoji: String? = null, - - @SerialName("archived_emoji") - val archivedEmoji: String? = null, - - @SerialName("archive_status_in_name") - val archiveStatusInName: Boolean = true, - - @SerialName("include_archived") - val includeArchived: Boolean = true, - - @SerialName("include_news") - val includeNews: Boolean = true, - - @SerialName("include_public") - val includePublic: Boolean = true, - - @SerialName("include_private") - val includePrivate: Boolean = false, - - @SerialName("include_hidden") - val includeHidden: Boolean = false, - - @SerialName("include_hidden_channels") - val includeHiddenChannels: Boolean = false, -) : Block() { - override suspend fun create(builder: MessageCreateBuilder) { - builder.content = text - - builder.embed { applyThreads() } - } - - override suspend fun edit(builder: MessageModifyBuilder) { - builder.content = text - builder.components = mutableListOf() - - builder.embed { applyThreads() } - } - - private suspend fun EmbedBuilder.applyThreads() { - val threads = getThreads() - - this.color = this@ThreadListBlock.color - this.title = this@ThreadListBlock.title - - description = buildString { - if (this@ThreadListBlock.description != null) { - append(this@ThreadListBlock.description) - append("\n\n") - } - - threads.forEach { thread -> - var line = template - .replace("{MENTION}", thread.mention) - .replace("{URL}", thread.getJumpUrl()) - .replace("{CREATED_TIME}", thread.id.timestamp.toDiscord(TimestampType.RelativeTime)) - .replace("{PARENT_ID}", thread.parentId.toString()) - .replace("{PARENT}", thread.parent.mention) - - if (thread.lastMessageId != null) { - line = line.replace( - "{ACTIVE_TIME}", - thread.lastMessageId!!.timestamp.toDiscord(TimestampType.RelativeTime) - ) - } - - line = if (archiveStatusInName && thread.isArchived) { - line.replace("{NAME}", thread.name + " (Archived)") - } else { - line.replace("{NAME}", thread.name) - } - - line = when { - thread.isArchived && archivedEmoji != null -> line.replace("{EMOJI}", archivedEmoji) - thread.isArchived.not() && activeEmoji != null -> line.replace("{EMOJI}", activeEmoji) - - else -> line.replace("{EMOJI}", "") - } - - appendLine(line) - } - } - } - - private suspend fun getThreads(): List { - var threads = guild.cachedThreads - .filter { thread -> - if (!includeHiddenChannels) { - val channel = thread.parent.asChannelOfOrNull() - - if (channel == null) { - false - } else { - val overwrite = channel.permissionOverwrites.firstOrNull { it.target == guild.id } - - overwrite == null || overwrite.denied.contains(Permission.ViewChannel).not() - } - } else { - true - } - } - .toList() - - threads = when (mode) { - ThreadListType.ACTIVE -> threads.sortedByDescending { it.lastMessage?.id?.timestamp } - ThreadListType.NEWEST -> threads.sortedByDescending { it.id.timestamp } - } - - if (!includeArchived) { - threads = threads.filter { !it.isArchived } - } - - if (!includeNews) { - threads = threads.filter { it.type != ChannelType.PublicNewsThread } - } - - if (!includePublic) { - threads = threads.filter { it.type != ChannelType.PublicGuildThread } - } - - if (!includePrivate) { - threads = threads.filter { it.type != ChannelType.PrivateThread } - } - - if (!includeHidden) { - threads = threads.filter { - it.getParent() - .getPermissionOverwritesForRole(it.guildId) - ?.denied - ?.contains(Permission.ViewChannel) != true - } - } - - return threads.take(limit) - } -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/config/SimpleWelcomeChannelConfig.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/config/SimpleWelcomeChannelConfig.kt deleted file mode 100644 index afdb6d47..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/config/SimpleWelcomeChannelConfig.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.config - -import com.kotlindiscord.kord.extensions.checks.types.Check -import dev.kord.core.entity.Guild -import dev.kord.core.entity.channel.GuildMessageChannel -import kotlinx.serialization.modules.PolymorphicModuleBuilder -import org.quiltmc.community.cozy.modules.welcome.blocks.Block -import kotlin.time.Duration - -internal typealias LogChannelGetter = (suspend (channel: GuildMessageChannel, guild: Guild) -> GuildMessageChannel?)? - -public class SimpleWelcomeChannelConfig(private val builder: Builder) : WelcomeChannelConfig() { - override val serializerBuilders: List.() -> Unit> = - builder.serializerBuilders - - override suspend fun getLoggingChannel(channel: GuildMessageChannel, guild: Guild): GuildMessageChannel? = - builder.loggingChannelGetter?.invoke(channel, guild) - - override suspend fun getStaffCommandChecks(): List> = builder.staffCommandChecks - override suspend fun getRefreshDelay(): Duration? = builder.refreshDuration - - public class Builder { - internal val staffCommandChecks: MutableList> = mutableListOf() - internal var loggingChannelGetter: LogChannelGetter = null - - public val serializerBuilders: MutableList.() -> Unit> = mutableListOf() - public var refreshDuration: Duration? = null - - public fun serializer(body: PolymorphicModuleBuilder.() -> Unit) { - serializerBuilders.add(body) - } - - public fun staffCommandCheck(body: Check<*>) { - staffCommandChecks.add(body) - } - - public fun getLogChannel(body: LogChannelGetter) { - loggingChannelGetter = body - } - } -} - -public fun SimpleWelcomeChannelConfig(body: SimpleWelcomeChannelConfig.Builder.() -> Unit): SimpleWelcomeChannelConfig { - val builder = SimpleWelcomeChannelConfig.Builder() - - body(builder) - - return SimpleWelcomeChannelConfig(builder) -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/config/WelcomeChannelConfig.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/config/WelcomeChannelConfig.kt deleted file mode 100644 index 19e1ab1e..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/config/WelcomeChannelConfig.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.config - -import com.kotlindiscord.kord.extensions.checks.types.Check -import dev.kord.core.entity.Guild -import dev.kord.core.entity.channel.GuildMessageChannel -import kotlinx.serialization.modules.PolymorphicModuleBuilder -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.polymorphic -import kotlinx.serialization.modules.subclass -import org.quiltmc.community.cozy.modules.welcome.blocks.* -import kotlin.time.Duration - -public abstract class WelcomeChannelConfig { - public open val serializerBuilders: List.() -> Unit> = mutableListOf() - - public val defaultSerializersModule: SerializersModule by lazy { - SerializersModule { - polymorphic(Block::class) { - serializerBuilders.forEach { it() } - - subclass(ComplianceBlock::class) - subclass(EmbedBlock::class) - subclass(LinksBlock::class) - subclass(MessageCopyBlock::class) - subclass(RolesBlock::class) - subclass(RulesBlock::class) - subclass(TextBlock::class) - subclass(ThreadListBlock::class) - } - } - } - - /** Get the configured logging channel for the given channel and guild. **/ - public abstract suspend fun getLoggingChannel(channel: GuildMessageChannel, guild: Guild): GuildMessageChannel? - - /** - * Get the configured staff command checks, used to ensure a staff-facing command can be run. - */ - public abstract suspend fun getStaffCommandChecks(): List> - - public abstract suspend fun getRefreshDelay(): Duration? - - /** - * Get the configured serializer module, which may be modified if other blocks have been set up. - */ - public open suspend fun getSerializersModule(): SerializersModule = defaultSerializersModule -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/data/MemoryWelcomeChannelData.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/data/MemoryWelcomeChannelData.kt deleted file mode 100644 index 88be53e6..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/data/MemoryWelcomeChannelData.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.data - -import dev.kord.common.entity.Snowflake - -public class MemoryWelcomeChannelData : WelcomeChannelData { - private val data: MutableMap = mutableMapOf() - - override suspend fun getChannelURLs(): Map = data - override suspend fun getUrlForChannel(channelId: Snowflake): String? = data[channelId] - override suspend fun removeChannel(channelId: Snowflake): String? = data.remove(channelId) - - override suspend fun setUrlForChannel(channelId: Snowflake, url: String) { - data[channelId] = url - } -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/data/WelcomeChannelData.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/data/WelcomeChannelData.kt deleted file mode 100644 index 39b380c0..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/data/WelcomeChannelData.kt +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.data - -import dev.kord.common.entity.Snowflake - -public interface WelcomeChannelData { - public suspend fun getChannelURLs(): Map - public suspend fun getUrlForChannel(channelId: Snowflake): String? - - public suspend fun setUrlForChannel(channelId: Snowflake, url: String) - public suspend fun removeChannel(channelId: Snowflake): String? -} diff --git a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/enums/ThreadListType.kt b/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/enums/ThreadListType.kt deleted file mode 100644 index 66b8f301..00000000 --- a/module-welcome/src/main/kotlin/org/quiltmc/community/cozy/modules/welcome/enums/ThreadListType.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.cozy.modules.welcome.enums - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -public enum class ThreadListType(public val humanReadable: String) { - @SerialName("active") - ACTIVE("Active"), - - @SerialName("newest") - NEWEST("Recently Created") -} diff --git a/settings.gradle.kts b/settings.gradle.kts index bfd2fb8a..ebf53d1e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,7 +18,5 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":module-log-parser") include(":module-moderation") include(":module-role-sync") -include(":module-tags") include(":module-user-cleanup") -include(":module-welcome") include(":module-ama") diff --git a/src/main/kotlin/org/quiltmc/community/App.kt b/src/main/kotlin/org/quiltmc/community/App.kt index 4f51311d..9ee65c39 100644 --- a/src/main/kotlin/org/quiltmc/community/App.kt +++ b/src/main/kotlin/org/quiltmc/community/App.kt @@ -14,6 +14,8 @@ import com.kotlindiscord.kord.extensions.modules.extra.mappings.extMappings import com.kotlindiscord.kord.extensions.modules.extra.phishing.DetectionAction import com.kotlindiscord.kord.extensions.modules.extra.phishing.extPhishing import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.extPluralKit +import com.kotlindiscord.kord.extensions.modules.extra.tags.tags +import com.kotlindiscord.kord.extensions.modules.extra.welcome.welcomeChannel import com.kotlindiscord.kord.extensions.utils.envOrNull import com.kotlindiscord.kord.extensions.utils.getKoin import dev.kord.core.entity.channel.GuildMessageChannel @@ -29,8 +31,6 @@ import org.quiltmc.community.cozy.modules.logs.processors.PiracyProcessor import org.quiltmc.community.cozy.modules.logs.processors.ProblematicLauncherProcessor import org.quiltmc.community.cozy.modules.moderation.moderation import org.quiltmc.community.cozy.modules.rolesync.rolesync -import org.quiltmc.community.cozy.modules.tags.tags -import org.quiltmc.community.cozy.modules.welcome.welcomeChannel import org.quiltmc.community.database.collections.AmaConfigCollection import org.quiltmc.community.database.collections.TagsCollection import org.quiltmc.community.database.collections.WelcomeChannelCollection @@ -101,7 +101,6 @@ suspend fun setupQuilt() = ExtensibleBot(DISCORD_TOKEN) { } extensions { - add(::ApplicationsExtension) add(::FilterExtension) add(::ForumExtension) add(::LookupExtension) @@ -114,7 +113,6 @@ suspend fun setupQuilt() = ExtensibleBot(DISCORD_TOKEN) { add(::SuggestionsExtension) add(::SyncExtension) add(::UtilityExtension) - add(::VerificationExtension) extPluralKit() diff --git a/src/main/kotlin/org/quiltmc/community/_Utils.kt b/src/main/kotlin/org/quiltmc/community/_Utils.kt index 2e206141..c61b1550 100644 --- a/src/main/kotlin/org/quiltmc/community/_Utils.kt +++ b/src/main/kotlin/org/quiltmc/community/_Utils.kt @@ -10,7 +10,10 @@ import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder import com.kotlindiscord.kord.extensions.commands.Arguments import com.kotlindiscord.kord.extensions.commands.application.slash.SlashCommandContext import com.kotlindiscord.kord.extensions.components.forms.ModalForm -import com.kotlindiscord.kord.extensions.utils.* +import com.kotlindiscord.kord.extensions.utils.env +import com.kotlindiscord.kord.extensions.utils.envOrNull +import com.kotlindiscord.kord.extensions.utils.getKoin +import com.kotlindiscord.kord.extensions.utils.loadModule import dev.kord.common.entity.ArchiveDuration import dev.kord.common.entity.Snowflake import dev.kord.common.entity.UserFlag @@ -255,7 +258,6 @@ fun Message.contentToThreadName(fallbackPrefix: String): String { ?: "$fallbackPrefix | $id" } -@Suppress("DEPRECATION_ERROR") // Either this, or the when block needs an else branch fun UserFlag.getName(): String = when (this) { UserFlag.ActiveDeveloper -> "Active Developer" UserFlag.BotHttpInteractions -> "HTTP-Based Commands" @@ -269,10 +271,10 @@ fun UserFlag.getName(): String = when (this) { UserFlag.HouseBravery -> "HypeSquad: Bravery" UserFlag.HouseBrilliance -> "HypeSquad: Brilliance" UserFlag.HypeSquad -> "HypeSquad" - UserFlag.System -> "System User" UserFlag.TeamUser -> "Team User" UserFlag.VerifiedBot -> "Verified Bot" UserFlag.VerifiedBotDeveloper -> "Early Verified Bot Developer" + is UserFlag.Unknown -> "Unknown" } diff --git a/src/main/kotlin/org/quiltmc/community/database/Migrations.kt b/src/main/kotlin/org/quiltmc/community/database/Migrations.kt index 4147c63a..fd2f23a4 100644 --- a/src/main/kotlin/org/quiltmc/community/database/Migrations.kt +++ b/src/main/kotlin/org/quiltmc/community/database/Migrations.kt @@ -64,6 +64,7 @@ object Migrations : KordExKoinComponent { 21 -> ::v21 22 -> ::v22 23 -> ::v23 + 24 -> ::v24 else -> break }(db.mongo) diff --git a/src/main/kotlin/org/quiltmc/community/database/collections/TagsCollection.kt b/src/main/kotlin/org/quiltmc/community/database/collections/TagsCollection.kt index c2ffa590..dd3df541 100644 --- a/src/main/kotlin/org/quiltmc/community/database/collections/TagsCollection.kt +++ b/src/main/kotlin/org/quiltmc/community/database/collections/TagsCollection.kt @@ -7,13 +7,13 @@ package org.quiltmc.community.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.kotlindiscord.kord.extensions.modules.extra.tags.data.Tag +import com.kotlindiscord.kord.extensions.modules.extra.tags.data.TagsData import dev.kord.common.entity.Snowflake import org.bson.conversions.Bson import org.koin.core.component.inject import org.litote.kmongo.eq import org.litote.kmongo.or -import org.quiltmc.community.cozy.modules.tags.data.Tag -import org.quiltmc.community.cozy.modules.tags.data.TagsData import org.quiltmc.community.database.Collection import org.quiltmc.community.database.Database import org.quiltmc.community.database.entities.TagEntity diff --git a/src/main/kotlin/org/quiltmc/community/database/collections/WelcomeChannelCollection.kt b/src/main/kotlin/org/quiltmc/community/database/collections/WelcomeChannelCollection.kt index fb7e4005..8b381266 100644 --- a/src/main/kotlin/org/quiltmc/community/database/collections/WelcomeChannelCollection.kt +++ b/src/main/kotlin/org/quiltmc/community/database/collections/WelcomeChannelCollection.kt @@ -7,10 +7,10 @@ package org.quiltmc.community.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.kotlindiscord.kord.extensions.modules.extra.welcome.data.WelcomeChannelData import dev.kord.common.entity.Snowflake import org.koin.core.component.inject import org.litote.kmongo.eq -import org.quiltmc.community.cozy.modules.welcome.data.WelcomeChannelData import org.quiltmc.community.database.Collection import org.quiltmc.community.database.Database import org.quiltmc.community.database.entities.WelcomeChannelEntity diff --git a/src/main/kotlin/org/quiltmc/community/database/entities/ServerSettings.kt b/src/main/kotlin/org/quiltmc/community/database/entities/ServerSettings.kt index 1c611e61..3bef0617 100644 --- a/src/main/kotlin/org/quiltmc/community/database/entities/ServerSettings.kt +++ b/src/main/kotlin/org/quiltmc/community/database/entities/ServerSettings.kt @@ -29,7 +29,6 @@ data class ServerSettings( var commandPrefix: String? = "?", val moderatorRoles: MutableSet = mutableSetOf(), - var verificationRole: Snowflake? = null, var cozyLogChannel: Snowflake? = null, var filterLogChannel: Snowflake? = null, @@ -157,14 +156,6 @@ data class ServerSettings( builder.append("\n") - builder.append("**Verification role:** ") - - if (verificationRole != null) { - builder.append("<@&$verificationRole>") - } else { - builder.append("N/A") - } - builder.append("\n\n") builder.append("**__Moderator Roles__**\n") diff --git a/src/main/kotlin/org/quiltmc/community/database/entities/TagEntity.kt b/src/main/kotlin/org/quiltmc/community/database/entities/TagEntity.kt index 444c0652..05cca359 100644 --- a/src/main/kotlin/org/quiltmc/community/database/entities/TagEntity.kt +++ b/src/main/kotlin/org/quiltmc/community/database/entities/TagEntity.kt @@ -11,11 +11,11 @@ package org.quiltmc.community.database.entities import com.github.jershell.kbson.UUIDSerializer +import com.kotlindiscord.kord.extensions.modules.extra.tags.data.Tag import dev.kord.common.Color import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers -import org.quiltmc.community.cozy.modules.tags.data.Tag import org.quiltmc.community.database.Entity @Serializable diff --git a/src/main/kotlin/org/quiltmc/community/database/migrations/v24.kt b/src/main/kotlin/org/quiltmc/community/database/migrations/v24.kt new file mode 100644 index 00000000..7ee4e7ea --- /dev/null +++ b/src/main/kotlin/org/quiltmc/community/database/migrations/v24.kt @@ -0,0 +1,19 @@ +/* + * 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 https://mozilla.org/MPL/2.0/. + */ + +package org.quiltmc.community.database.migrations + +import com.mongodb.client.model.Updates +import org.litote.kmongo.EMPTY_BSON +import org.litote.kmongo.coroutine.CoroutineDatabase +import org.quiltmc.community.database.collections.ServerSettingsCollection + +suspend fun v24(db: CoroutineDatabase) { + db.getCollection(ServerSettingsCollection.name).updateMany( + EMPTY_BSON, + Updates.unset("verificationRole") + ) +} diff --git a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/ApplicationsExtension.kt b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/ApplicationsExtension.kt deleted file mode 100644 index b0c01e55..00000000 --- a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/ApplicationsExtension.kt +++ /dev/null @@ -1,869 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.modes.quilt.extensions - -import com.kotlindiscord.kord.extensions.* -import com.kotlindiscord.kord.extensions.checks.failed -import com.kotlindiscord.kord.extensions.checks.guildFor -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.member -import com.kotlindiscord.kord.extensions.commands.converters.impl.user -import com.kotlindiscord.kord.extensions.events.extra.GuildJoinRequestDeleteEvent -import com.kotlindiscord.kord.extensions.events.extra.GuildJoinRequestUpdateEvent -import com.kotlindiscord.kord.extensions.events.extra.models.ApplicationStatus -import com.kotlindiscord.kord.extensions.events.extra.models.GuildJoinRequestResponse -import com.kotlindiscord.kord.extensions.extensions.* -import com.kotlindiscord.kord.extensions.time.TimestampType -import com.kotlindiscord.kord.extensions.time.toDiscord -import com.kotlindiscord.kord.extensions.utils.* -import dev.kord.common.Color -import dev.kord.common.entity.ButtonStyle -import dev.kord.common.entity.Snowflake -import dev.kord.core.behavior.channel.createEmbed -import dev.kord.core.behavior.channel.createMessage -import dev.kord.core.behavior.edit -import dev.kord.core.behavior.getChannelOf -import dev.kord.core.behavior.interaction.response.createEphemeralFollowup -import dev.kord.core.builder.components.emoji -import dev.kord.core.entity.Member -import dev.kord.core.entity.ReactionEmoji -import dev.kord.core.entity.User -import dev.kord.core.entity.channel.TextChannel -import dev.kord.core.entity.channel.TopGuildMessageChannel -import dev.kord.core.event.interaction.ButtonInteractionCreateEvent -import dev.kord.rest.builder.message.EmbedBuilder -import dev.kord.rest.builder.message.actionRow -import dev.kord.rest.builder.message.embed -import io.github.oshai.kotlinlogging.KotlinLogging -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.toList -import kotlinx.datetime.Instant -import org.koin.core.component.inject -import org.quiltmc.community.GUILDS -import org.quiltmc.community.database.collections.ServerApplicationCollection -import org.quiltmc.community.database.collections.ServerSettingsCollection -import org.quiltmc.community.database.entities.ServerApplication -import org.quiltmc.community.database.entities.ServerSettings -import org.quiltmc.community.hasBaseModeratorRole -import org.quiltmc.community.inQuiltGuild -import kotlin.time.Duration.Companion.seconds - -private val COMPONENT_REGEX = "application/(\\d+)/(thread|verify)".toRegex() - -class ApplicationsExtension : Extension() { - override val name: String = "applications" - - private val logger = KotlinLogging.logger("org.quiltmc.community.modes.quilt.extensions.ApplicationsExtension") - - private val serverSettings: ServerSettingsCollection by inject() - private val applications: ServerApplicationCollection by inject() - - private val Instant.longAndRelative - get() = "${toDiscord(TimestampType.LongDateTime)} (${toDiscord(TimestampType.RelativeTime)})" - - override suspend fun setup() { - GUILDS.forEach { - ephemeralSlashCommand { - name = "applications" - description = "Commands related to managing server applications" - allowInDms = false - - guild(it) - - check { inQuiltGuild() } - check { hasBaseModeratorRole(false) } - - ephemeralSubCommand(::ForceVerifyArguments) { - name = "force-verify" - description = "Make a user bypass Discord's verification process" - - action { - val settings = serverSettings.get(guild!!.id) - - if (settings?.verificationRole == null || settings.moderationLogChannel == null) { - logger.debug { - "Guild ${guild?.id} doesn't have a verification role or moderation logging " + - "channel configured." - } - - respond { - content = - "This server doesn't have a configured verification role or moderation logging " + - "channel." - } - - return@action - } - - val member = guild!!.getMemberOrNull(arguments.user.id) - val modLog = guild!!.getChannelOf(settings.moderationLogChannel!!) - - if (member == null) { - logger.warn { - "User ${arguments.user.id} is not present on guild ${guild?.id}" - } - - respond { - content = "User is not in this guild." - } - - return@action - } - - if (settings.verificationRole in member.roleIds) { - respond { - content = "This user has already been verified." - } - - return@action - } - - member.addRole(settings.verificationRole!!) - - modLog.createEmbed { - title = "User force verified" - color = DISCORD_BLURPLE - - field { - inline = true - name = "Moderator" - value = "${user.asUser().tag} (${user.mention})" - } - - field { - inline = true - name = "User" - value = "${member.tag} (${member.mention})" - } - } - - logger.info { "User force-verified: ${member.id}" } - - respond { - content = "User ${member.mention} has been force verified." - } - } - } - - ephemeralSubCommand(::LookupArguments) { - name = "lookup" - description = "Look up a user's previous applications" - - action { - val previousApplications = applications.findByUser(arguments.user.id).toList() - - logger.debug { "Found ${previousApplications.size} applications for user ${arguments.user.id}" } - - if (previousApplications.isEmpty()) { - respond { - content = "No applications found for ${arguments.user.mention}." - } - - return@action - } - - editingPaginator { - previousApplications.forEach { app -> - page { - title = "Previous Applications" - color = app.status.toColor() - - description = buildString { - appendLine("**Created:** ${app._id.timestamp.longAndRelative}") - appendLine("**Status:** ${app.status?.name?.capitalizeWords() ?: "Withdrawn"}") - appendLine("**User:** <@${app.userId}>") - appendLine() - - if (app.actionedAt != null) { - appendLine("**Actioned:** ${app.actionedAt!!.longAndRelative}") - appendLine() - } - - if (app.rejectionReason != null) { - appendLine("**Rejection reason**") - appendLine( - app.rejectionReason!! - .lines() - .joinToString("\n") { "> $it" } - ) - appendLine() - } - - if (app.messageLink != null) { - appendLine("[More details...](${app.messageLink})") - } - } - } - } - }.send() - } - } - } - - ephemeralMessageCommand { - name = "Fix Application Message" - allowInDms = false - - guild(it) - - check { inQuiltGuild() } - check { hasBaseModeratorRole(false) } - - action { - val message = targetMessages.first() - val application = applications.getByMessage(message.id) - - if (application == null) { - logger.debug { - "No application found for message: ${message.id}" - } - - respond { - content = "Unable to find an application for this message." - } - - return@action - } - - val otherApplications = applications - .findByUser(application.userId) - .filter { it._id != application._id } - .toList() - - logger.info { - "Fixing content and buttons for message ${message.id} with application ${application._id}" - } - - message.edit { - embed { message.embeds.first().apply(this) } - - if (otherApplications.isNotEmpty()) { - embed { addOther(otherApplications) } - } - - actionRow { - interactionButton( - ButtonStyle.Secondary, - "application/${application._id}/thread" - ) { - emoji(ReactionEmoji.Unicode("✉️")) - - label = "Create Thread" - } - - if (application.status == ApplicationStatus.Submitted) { - interactionButton( - ButtonStyle.Success, - "application/${application._id}/verify" - ) { - emoji(ReactionEmoji.Unicode("✅")) - - label = "Force Verify" - } - } - } - } - - respond { - content = "Message updated." - } - } - } - } - - chatCommand(::ForceVerifyArguments) { - name = "force-verify" - description = "Make a user bypass Discord's verification process" - - check { inQuiltGuild() } - check { hasBaseModeratorRole(false) } - - action { - val settings = serverSettings.get(guild!!.id) - - if (settings?.verificationRole == null || settings.moderationLogChannel == null) { - logger.debug { - "Guild ${guild?.id} doesn't have a verification role or moderation logging " + - "channel configured." - } - - message.respond { - content = "This server doesn't have a configured verification role or moderation logging " + - "channel." - } - - return@action - } - - val member = guild!!.getMemberOrNull(arguments.user.id) - val modLog = guild!!.getChannelOf(settings.moderationLogChannel!!) - - if (member == null) { - logger.warn { - "User ${arguments.user.id} is not present on guild ${guild?.id}" - } - - message.respond { - content = "User is not in this guild." - } - } else { - if (settings.verificationRole in member.roleIds) { - message.respond { - content = "This user has already been verified." - } - - return@action - } - - member.addRole(settings.verificationRole!!) - - modLog.createEmbed { - title = "User force verified" - color = DISCORD_BLURPLE - - field { - inline = true - name = "Moderator" - value = "${user!!.asUser().tag} (${user!!.mention})" - } - - field { - inline = true - name = "User" - value = "${member.tag} (${member.mention})" - } - } - - logger.info { "User force-verified: ${member.id}" } - - message.respond { - content = "User ${member.mention} has been force verified." - } - } - } - } - - event { - check { inQuiltGuild() } - check { hasBaseModeratorRole(false) } - - check { - logger.debug { "Received interaction for button: ${event.interaction.componentId}" } - - val match = COMPONENT_REGEX.matchEntire(event.interaction.componentId) - - if (match == null) { - logger.failed("Button interaction didn't match the component ID regex") - fail("Button interaction didn't match the component ID regex") - - return@check - } - - val guild = guildFor(event)!! - val settings = serverSettings.get(guild.id) - - if (settings?.applicationLogChannel == null || settings.applicationThreadsChannel == null) { - logger.failed("Guild ${guild.id} does not have an application log or threads channel configured") - fail("Guild ${guild.id} does not have an application log or threads channel configured.") - - return@check - } - - val applicationId = Snowflake(match.groupValues[1]) - val action = match.groupValues[2] - - val app = applications.get(applicationId) - - if (app == null) { - logger.failed("Unknown application: $applicationId") - fail("Unknown application: $applicationId") - - return@check - } - - cache["action"] = action - cache["application"] = app - cache["serverSettings"] = settings - } - - action { - val response = event.interaction.ackEphemeral() - - val action = cache.getOfOrNull("action")!! - val application = cache.getOfOrNull("application")!! - val settings = cache.getOfOrNull("serverSettings")!! - - val guild = guildFor(event)!! - val user = kord.getUser(application.userId) - - if (user == null) { - logger.warn { - "User ${application.userId} that created application ${application._id} can't be found" - } - - response.createEphemeralFollowup { - content = "User that created this application can't be found or no longer exists." - } - - return@action - } - - when (action) { - "thread" -> { - val threadChannel = guild.getChannelOf(settings.applicationThreadsChannel!!) - - if (application.threadId != null) { - logger.debug { - "Application ${application._id} already has a thread." - } - - response.createEphemeralFollowup { - content = "A thread already exists for this application: <#${application.threadId}>" - } - } else { - logger.info { "Creating thread for application: ${application._id}" } - - // Not actually deprecated, Kord walled themselves into a hole here - @Suppress("DEPRECATION_ERROR") - val thread = threadChannel.startPrivateThread("App: ${user.tag}") - val initialMessage = thread.createMessage("Better get the mods in...") - - initialMessage.edit { content = settings.moderatorRoles.joinToString { "<@&$it>" } } - delay(2.seconds) - - initialMessage.edit { - content = buildString { - appendLine("**Application thread for ${user.tag}**") - append("User ID below.") - } - } - - thread.createMessage("`${user.id}`") - - response.createEphemeralFollowup { - content = "Thread created: ${thread.mention}" - } - - application.threadId = thread.id - applications.save(application) - } - } - - "verify" -> { - if (settings.verificationRole == null || settings.moderationLogChannel == null) { - logger.debug { - "Guild ${guild.id} doesn't have a verification role or moderation logging " + - "channel configured." - } - - response.createEphemeralFollowup { - content = - "This server doesn't have a configured verification role or moderation logging " + - "channel." - } - - return@action - } - - val member = guild.getMemberOrNull(application.userId) - - if (member == null) { - logger.warn { - "User ${application.userId} is not present on guild ${guild.id}" - } - - response.createEphemeralFollowup { - content = "User is not in this guild." - } - - return@action - } - - if (settings.verificationRole in member.roleIds) { - response.createEphemeralFollowup { - content = "This user has already been verified." - } - - return@action - } - - val modLog = guild.getChannelOf(settings.moderationLogChannel!!) - member.addRole(settings.verificationRole!!) - - modLog.createEmbed { - title = "User force verified" - color = DISCORD_BLURPLE - - field { - inline = true - name = "Moderator" - value = "${event.interaction.user.tag} (${event.interaction.user.mention})" - } - - field { - inline = true - name = "User" - value = "${member.tag} (${member.mention})" - } - } - - logger.info { "User force-verified: ${member.id}" } - - response.createEphemeralFollowup { - content = "User ${member.mention} has been force verified." - } - } - - else -> { - logger.warn { "Unknown application button action: $action" } - - response.createEphemeralFollowup { - content = "Unknown application button action: $action" - } - } - } - } - } - - event { - check { inQuiltGuild() } - - check { - val guild = guildFor(event)!! - val settings = serverSettings.get(guild.id) - - if (settings?.applicationLogChannel == null || settings.applicationThreadsChannel == null) { - logger.failed("Guild ${guild.id} does not have an application log or threads channel configured") - fail("Guild ${guild.id} does not have an application log or threads channel configured.") - - return@check - } - - val app = applications.get(event.requestId) - - if (app != null) { - cache["application"] = app - } - - cache["serverSettings"] = settings - } - - action { - delay(1.seconds) // Just to make sure we get the update first - - val application = cache.getOfOrNull("application") - val settings = cache.getOfOrNull("serverSettings")!! - - val logChannel = event.getGuild().getChannelOf(settings.applicationLogChannel!!) - - var otherApplications = applications - .findByUser(event.userId) - .toList() - - // If null, an application was deleted that we're not keeping track of - happens when someone - // leaves without applying as well, so we can't log it on Discord really - - if (application != null && application.status == ApplicationStatus.Submitted) { - logger.debug { "Marking submitted application as withdrawn: ${event.requestId}" } - - val message = logChannel.getMessage(application.messageId) - - otherApplications = otherApplications - .filter { it._id != application._id } - - message.edit { - embed { - message.embeds.first().apply(this) - - title = "Application (Withdrawn)" - color = DISCORD_WHITE - } - - if (otherApplications.isNotEmpty()) { - embed { addOther(otherApplications) } - } - - actionRow { - interactionButton( - ButtonStyle.Secondary, - "application/${application._id}/thread" - ) { - emoji(ReactionEmoji.Unicode("✉️")) - - label = "Create Thread" - } - } - } - - application.status = null - applications.save(application) - } - } - } - - event { - check { inQuiltGuild() } - - check { - val guild = guildFor(event)!! - val settings = serverSettings.get(guild.id) - - if (settings?.applicationLogChannel == null || settings.applicationThreadsChannel == null) { - logger.failed("Guild ${guild.id} does not have an application log or threads channel configured") - fail("Guild ${guild.id} does not have an application log or threads channel configured.") - - return@check - } - - val app = applications.get(event.requestId) - - if (app != null) { - cache["application"] = app - } - - cache["serverSettings"] = settings - } - - action { - var application = cache.getOfOrNull("application") - val settings = cache.getOfOrNull("serverSettings")!! - - val logChannel = event.getGuild().getChannelOf(settings.applicationLogChannel!!) - var otherApplications = applications - .findByUser(event.userId) - .toList() - - if (application == null) { - logger.debug { "Saving new application and sending to channel: ${event.requestId}" } - - val message = logChannel.createMessage { - embed { fromEvent(event) } - - if (otherApplications.isNotEmpty()) { - embed { addOther(otherApplications) } - } - - actionRow { - interactionButton( - ButtonStyle.Secondary, - "application/${event.request.id}/thread" - ) { - emoji(ReactionEmoji.Unicode("✉️")) - - label = "Create Thread" - } - - if (event.status == ApplicationStatus.Submitted) { - interactionButton( - ButtonStyle.Success, - "application/${event.request.id}/verify" - ) { - emoji(ReactionEmoji.Unicode("✅")) - - label = "Force Verify" - } - } - } - } - - application = ServerApplication( - _id = event.requestId, - status = event.status, - - guildId = event.guildId, - messageId = message.id, - userId = event.userId, - messageLink = message.getJumpUrl() - ) - - if (event.status == ApplicationStatus.Rejected) { - application.actionedAt = event.request.actionedAt - application.rejectionReason = event.request.rejectionReason - } - - applications.save(application) - } else { - logger.debug { "Updating existing application: ${event.requestId}" } - - otherApplications = otherApplications - .filter { it._id != application._id } - - val message = logChannel.getMessage(application.messageId) - - message.edit { - embed { fromEvent(event) } - - if (otherApplications.isNotEmpty()) { - embed { addOther(otherApplications) } - } - - actionRow { - interactionButton( - ButtonStyle.Secondary, - "application/${event.request.id}/thread" - ) { - emoji(ReactionEmoji.Unicode("✉️")) - - label = "Create Thread" - } - - if (event.status == ApplicationStatus.Submitted) { - interactionButton( - ButtonStyle.Success, - "application/${event.request.id}/verify" - ) { - emoji(ReactionEmoji.Unicode("✅")) - - label = "Force Verify" - } - } - } - } - - application.actionedAt = event.request.actionedAt - application.rejectionReason = event.request.rejectionReason - application.status = event.request.status - - applications.save(application) - } - } - } - } - - private fun ApplicationStatus?.toColor(): Color = when (this) { - ApplicationStatus.Approved -> DISCORD_GREEN - ApplicationStatus.Rejected -> DISCORD_RED - ApplicationStatus.Submitted -> DISCORD_YELLOW - - null -> DISCORD_WHITE - } - - private fun EmbedBuilder.addOther(apps: List) { - title = "Other Applications: ${apps.size}" - color = DISCORD_BLURPLE - - description = buildString { - apps.sortedBy { it._id.timestamp }.forEachIndexed { i, app -> - if (app.messageLink != null) { - append("[${i + 1})](${app.messageLink}) ") - } else { - append("${i + 1}) ") - } - - append(app.status?.name?.capitalizeWords() ?: "Withdrawn") - - if (app.actionedAt != null) { - append( - " at ${app.actionedAt!!.longAndRelative}" - ) - } - - appendLine() - - if (app.rejectionReason != null) { - appendLine( - app.rejectionReason!! - .lines() - .joinToString("\n") { "> $it" } - ) - } - - appendLine() - } - } - } - - private suspend fun EmbedBuilder.fromEvent(event: GuildJoinRequestUpdateEvent) { - val user = event.getUser() - - color = event.request.status.toColor() - title = "Application (${event.request.status.name.capitalizeWords()})" - - description = buildString { - appendLine("**User:** ${user.tag}") - appendLine("**Mention:** ${user.mention}") - appendLine( - "**Created:** ${user.id.timestamp.longAndRelative}" - ) - - if (event.request.actionedByUser != null) { - val moderator = event.request.actionedByUser!! - val time = event.request.actionedAt!! - - appendLine() - appendLine( - "**Actioned at:** ${time.longAndRelative}" - ) - - if (event.request.requestBypassed) { - appendLine( - "**Application bypassed via role assignment** " - ) - } else { - appendLine( - "**Actioned by:** <@${moderator.id}> (`${moderator.username}#${moderator.discriminator}`)" - ) - } - - if (event.request.rejectionReason != null) { - appendLine() - appendLine("**Rejection reason**") - append( - event.request.rejectionReason!! - .lines() - .joinToString("\n") { "> $it" } - ) - } - } - } - - thumbnail { - url = (user.avatar ?: user.defaultAvatar).cdnUrl.toUrl() - } - - footer { - text = "User ID: ${event.userId}" - } - - event.request.formResponses.forEach { - field { - name = it.label - - value = when (it) { - is GuildJoinRequestResponse.TermsResponse -> - if (it.response) { - "✅ Accepted" - } else { - "❌ Not accepted" - } - - is GuildJoinRequestResponse.MultipleChoiceResponse -> - it.choices[it.response] - - is GuildJoinRequestResponse.ParagraphResponse -> - it.response - } - } - } - } - - inner class LookupArguments : Arguments() { - val user: User by user { - name = "user" - description = "Member to look up applications for" - } - } - - inner class ForceVerifyArguments : Arguments() { - val user: Member by member { - name = "member" - description = "Member to verify" - } - } -} diff --git a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/VerificationExtension.kt b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/VerificationExtension.kt deleted file mode 100644 index 91cd7b26..00000000 --- a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/VerificationExtension.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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 https://mozilla.org/MPL/2.0/. - */ - -package org.quiltmc.community.modes.quilt.extensions - -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.event -import dev.kord.core.event.guild.GuildCreateEvent -import dev.kord.core.event.guild.MemberUpdateEvent -import io.github.oshai.kotlinlogging.KotlinLogging -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter -import org.koin.core.component.inject -import org.quiltmc.community.database.collections.ServerSettingsCollection -import org.quiltmc.community.inQuiltGuild -import kotlin.time.Duration.Companion.minutes - -class VerificationExtension : Extension() { - override val name: String = "verification" - - private val logger = KotlinLogging.logger { } - private val serverSettings: ServerSettingsCollection by inject() - - override suspend fun setup() { - event { - check { inQuiltGuild() } - - action { - val roleId = serverSettings.get(event.guild.id)?.verificationRole - val guild = event.guild - - if (roleId == null) { - logger.debug { "Guild ${guild.name} (${guild.id}) has no verification role set." } - - return@action - } - - logger.debug { "Waiting for a minute before syncing verification roles..." } - - delay(1.minutes) - - guild.members - .filter { !it.isPending && roleId !in it.roleIds } - .collect { - it.addRole(roleId, "Member has passed screening") - logger.debug { "Verification role applied to user: ${it.id}" } - } - } - } - - event { - check { inQuiltGuild() } - check { failIf(event.member.isPending, "Member is pending") } - - action { - val roleId = serverSettings.get(event.guildId)?.verificationRole - - if (roleId == null) { - val guild = event.guild.asGuild() - - logger.debug { "Guild ${guild.name} (${guild.id}) has no verification role set." } - - return@action - } - - if (event.guild.getRoleOrNull(roleId) == null) { - val guild = event.guild.asGuild() - - logger.warn { - "Guild ${guild.name} (${guild.id}) has a verification role set, but the role has been deleted." - } - - return@action - } - - if (!event.member.isPending && roleId !in event.member.roleIds) { - event.member.addRole(roleId, "Member has passed screening") - - logger.debug { "Verification role applied to user: ${event.member.id}" } - } - } - } - } -} diff --git a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/filtering/FilterExtension.kt b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/filtering/FilterExtension.kt index 471c5f77..83e6bd7e 100644 --- a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/filtering/FilterExtension.kt +++ b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/filtering/FilterExtension.kt @@ -85,7 +85,6 @@ class FilterExtension : Extension() { init { RgxGenOption.INFINITE_PATTERN_REPETITION.setInProperties(rgxProperties, 2) - RgxGen.setDefaultProperties(rgxProperties) } val filters: FilterCollection by inject() @@ -1105,7 +1104,7 @@ class FilterExtension : Extension() { if (filter.matchType == MatchType.REGEX || filter.matchType == MatchType.REGEX_CONTAINS) { try { - val generator = RgxGen(filter.match) + val generator = RgxGen.parse(rgxProperties, filter.match) val examples = mutableSetOf() repeat(FILTERS_PER_PAGE * 2) { diff --git a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/github/GithubExtension.kt b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/github/GithubExtension.kt index 7e039a93..4f082481 100644 --- a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/github/GithubExtension.kt +++ b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/github/GithubExtension.kt @@ -37,7 +37,7 @@ import org.quiltmc.community.modes.quilt.extensions.github.types.GitHubSimpleUse import quilt.ghgen.DeleteIssue import quilt.ghgen.FindIssueId import quilt.ghgen.findissueid.* -import java.net.URL +import java.net.URI private const val USERS_PER_PAGE = 10 private const val BLOCKS_URL = "https://api.github.com/orgs/{ORG}/blocks" @@ -48,7 +48,7 @@ class GithubExtension : Extension() { val logger = KotlinLogging.logger { } private val graphQlClient = GraphQLKtorClient( - URL("https://api.github.com/graphql"), + URI.create("https://api.github.com/graphql").toURL(), HttpClient(engineFactory = CIO) { defaultRequest { diff --git a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/minecraft/MinecraftExtension.kt b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/minecraft/MinecraftExtension.kt index ff1a03ce..9130529c 100644 --- a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/minecraft/MinecraftExtension.kt +++ b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/minecraft/MinecraftExtension.kt @@ -33,6 +33,7 @@ import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.serialization.kotlinx.json.* import kotlinx.datetime.Clock +import kotlinx.serialization.json.Json import org.apache.commons.text.StringEscapeUtils import org.quiltmc.community.* @@ -60,7 +61,11 @@ class MinecraftExtension : Extension() { private val client = HttpClient { install(ContentNegotiation) { - json() + json( + Json { + ignoreUnknownKeys = true + } + ) } expectSuccess = true @@ -294,7 +299,7 @@ class MinecraftExtension : Extension() { return result to 0 } - private suspend fun EmbedBuilder.patchNotes(patchNote: PatchNote, maxLength: Int = 1000) { + private fun EmbedBuilder.patchNotes(patchNote: PatchNote, maxLength: Int = 1000) { val (truncated, remaining) = patchNote.body.formatHTML().truncateMarkdown(maxLength) title = patchNote.title diff --git a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/settings/SettingsExtension.kt b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/settings/SettingsExtension.kt index 138f506b..684f7f6e 100644 --- a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/settings/SettingsExtension.kt +++ b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/settings/SettingsExtension.kt @@ -945,40 +945,6 @@ class SettingsExtension : Extension() { } } - ephemeralSubCommand(::SingleRoleArg) { - name = "verification-role" - description = "For Quilt servers: Set (or clear) the verification role" - - check { hasPermissionInMainGuild(Permission.Administrator) } - - action { - val settings = if (arguments.serverId == null) { - serverSettings.get(guild!!.id) - } else { - serverSettings.get(arguments.serverId!!) - } - - if (settings == null) { - respond { - content = ":x: Unknown guild ID: `${arguments.serverId}`" - } - - return@action - } - - settings.verificationRole = arguments.role?.id - settings.save() - - respond { - content = if (settings.verificationRole == null) { - "**Verification role unset**" - } else { - "**Verification role set:** <@&${settings.verificationRole}>" - } - } - } - } - ephemeralSubCommand(::ShouldLeaveArg) { name = "set-leave-server" description = "For Quilt servers: Set whether Cozy should automatically leave a server" @@ -1173,16 +1139,4 @@ class SettingsExtension : Extension() { description = "Server ID, if not the current one" } } - - inner class SingleRoleArg : Arguments() { - val role by optionalRole { - name = "role" - description = "Role to set, omit to clear" - } - - val serverId by optionalSnowflake { - name = "server" - description = "Server ID, if not the current one" - } - } }