From c2218d909b8a9d33aeaa0e7d29711a17cc5b7670 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Sun, 18 Jul 2021 17:33:11 +0200 Subject: [PATCH] Feature/improvments (#74) * Bump dependencies - Fix detekt issues - Enable detekt autocorrect * Enable detekt for sub-projects * Fix slash command registration * Implement tags user ping (Fix #60) * Fix tag-list command ignoring range (Fix #65) * Make paginator context-aware to use buttons in slash command context * Run detekt * Bump version * Fix detekt --- .github/workflows/ci.yml | 2 +- autohelp/build.gradle.kts | 6 +- autohelp/kord/build.gradle.kts | 10 +- .../schlaubi/autohelp/kord/KordEventSource.kt | 1 - .../autohelp/kord/KordUpdateEventSource.kt | 6 +- .../kotlin/me/schlaubi/autohelp/Builder.kt | 2 +- .../schlaubi/autohelp/help/OutgoingMessage.kt | 9 +- .../autohelp/internal/AutoHelpImpl.kt | 1 + .../autohelp/internal/EventHandler.kt | 1 + .../schlaubi/autohelp/source/EventContext.kt | 2 +- .../me/schlaubi/autohelp/tags/TagSupplier.kt | 2 +- build.gradle.kts | 88 ++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- .../kotlin/de/nycode/bankobot/BankoBot.kt | 4 +- .../command/BankoBotContextConverter.kt | 3 +- .../de/nycode/bankobot/command/Context.kt | 2 +- .../nycode/bankobot/command/ErrorHandlers.kt | 14 +- .../bankobot/command/slashcommands/Creator.kt | 4 + .../slashcommands/InteractionEventHandler.kt | 20 +- .../command/slashcommands/Pipeline.kt | 8 +- .../command/slashcommands/SlashArgument.kt | 3 + .../slashcommands/SlashCommandContext.kt | 26 +-- .../arguments/AbstractSlashCommandArgument.kt | 4 + .../nycode/bankobot/commands/TestCommand.kt | 2 +- .../bankobot/commands/dev/EvalCommand.kt | 2 +- .../bankobot/commands/general/DocsCommand.kt | 5 +- .../commands/general/JDoodleCommand.kt | 3 +- .../commands/general/SearchCommand.kt | 5 +- .../commands/tag/commands/ListTagsCommand.kt | 12 +- .../tag/commands/SearchTagsCommand.kt | 6 +- .../commands/tag/commands/TagCommand.kt | 22 +- .../tag/commands/TagsFromUserCommand.kt | 6 +- .../tag/commands/TransferTagCommand.kt | 3 +- .../de/nycode/bankobot/docdex/DocsGoogle.kt | 19 +- .../bankobot/twitch/TwitchIntegration.kt | 13 +- .../nycode/bankobot/twitch/WebhookServer.kt | 2 - .../de/nycode/bankobot/utils/CodeBlockUtil.kt | 2 +- .../de/nycode/bankobot/utils/Constants.kt | 6 +- .../de/nycode/bankobot/utils/GitHubUtil.kt | 8 +- .../de/nycode/bankobot/utils/JDoodleUtil.kt | 2 +- .../de/nycode/bankobot/utils/LoadingUtil.kt | 4 +- .../de/nycode/bankobot/utils/StringUtil.kt | 4 +- .../utils/paginator/AbstractPaginator.kt | 202 +++++++++++++++++ .../utils/paginator/InteractionsPaginator.kt | 119 ++++++++++ .../utils/paginator/MessagePaginator.kt | 106 +++++++++ .../utils/{ => paginator}/Paginator.kt | 212 ++++-------------- .../variables/parsers/calc/CalcExpression.kt | 9 +- 47 files changed, 675 insertions(+), 319 deletions(-) create mode 100644 src/main/kotlin/de/nycode/bankobot/utils/paginator/AbstractPaginator.kt create mode 100644 src/main/kotlin/de/nycode/bankobot/utils/paginator/InteractionsPaginator.kt create mode 100644 src/main/kotlin/de/nycode/bankobot/utils/paginator/MessagePaginator.kt rename src/main/kotlin/de/nycode/bankobot/utils/{ => paginator}/Paginator.kt (50%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67b5c87..1e7707e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,4 +24,4 @@ jobs: ~/build ~/.gradle - name: Build with Gradle - run: ./gradlew compileKotlin detektMain check + run: ./gradlew compileKotlin detekt check diff --git a/autohelp/build.gradle.kts b/autohelp/build.gradle.kts index 12f1ab5..b7db23f 100644 --- a/autohelp/build.gradle.kts +++ b/autohelp/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "me.schlaubi" -version = "2.0.0-RC.4" +version = "2.0.0-RC.5" repositories { mavenCentral() @@ -15,8 +15,8 @@ repositories { dependencies { api("dev.schlaubi.forp", "forp-analyze-api-jvm", "1.0-SNAPSHOT") - api("dev.kord.x", "emoji", "0.5.0-SNAPSHOT") - implementation("io.github.microutils", "kotlin-logging", "1.12.0") + api("dev.kord.x", "emoji", "0.5.0") + implementation("io.github.microutils", "kotlin-logging", "2.0.10") } kotlin { diff --git a/autohelp/kord/build.gradle.kts b/autohelp/kord/build.gradle.kts index fbfbe7f..bd7b17a 100644 --- a/autohelp/kord/build.gradle.kts +++ b/autohelp/kord/build.gradle.kts @@ -38,17 +38,23 @@ repositories { dependencies { api(project(":autohelp")) - api("dev.kord", "kord-core", "kotlin-1.5-SNAPSHOT") + api("dev.kord", "kord-core", "0.7.3") } tasks { withType { kotlinOptions { - jvmTarget = "11" + jvmTarget = "15" } } } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(15)) // kapt dies on JDK 16 + } +} + kotlin { explicitApi() } diff --git a/autohelp/kord/src/main/kotlin/me/schlaubi/autohelp/kord/KordEventSource.kt b/autohelp/kord/src/main/kotlin/me/schlaubi/autohelp/kord/KordEventSource.kt index ba0f618..036e4cf 100644 --- a/autohelp/kord/src/main/kotlin/me/schlaubi/autohelp/kord/KordEventSource.kt +++ b/autohelp/kord/src/main/kotlin/me/schlaubi/autohelp/kord/KordEventSource.kt @@ -49,7 +49,6 @@ public class KordEventSource(kord: Kord) : EventSource { override val events: Flow = kord.events .filterIsInstance() .map { KordReceivedMessage(it.message) } - } /** diff --git a/autohelp/kord/src/main/kotlin/me/schlaubi/autohelp/kord/KordUpdateEventSource.kt b/autohelp/kord/src/main/kotlin/me/schlaubi/autohelp/kord/KordUpdateEventSource.kt index fa30d48..063fa6e 100644 --- a/autohelp/kord/src/main/kotlin/me/schlaubi/autohelp/kord/KordUpdateEventSource.kt +++ b/autohelp/kord/src/main/kotlin/me/schlaubi/autohelp/kord/KordUpdateEventSource.kt @@ -49,8 +49,8 @@ public class KordUpdateMessage(public val kordMessage: Message) : ReceivedMessag get() = kordMessage.data.guildId.value!!.value override val channelId: Long get() = kordMessage.data.channelId.value - override val authorId: Long - get() = kordMessage.author?.id?.value!! + override val authorId: Long? + get() = kordMessage.author?.id?.value override val content: String? = null override val files: List get() = kordMessage.embeds.mapNotNull { @@ -62,4 +62,4 @@ public class KordUpdateMessage(public val kordMessage: Message) : ReceivedMessag } override suspend fun react(emoji: DiscordEmoji): Unit = kordMessage.addReaction(emoji) -} \ No newline at end of file +} diff --git a/autohelp/src/main/kotlin/me/schlaubi/autohelp/Builder.kt b/autohelp/src/main/kotlin/me/schlaubi/autohelp/Builder.kt index 05ed62b..261e02c 100644 --- a/autohelp/src/main/kotlin/me/schlaubi/autohelp/Builder.kt +++ b/autohelp/src/main/kotlin/me/schlaubi/autohelp/Builder.kt @@ -165,4 +165,4 @@ public class ContextBuilder { public fun filter(eventFilter: EventFilter): Unit = +eventFilter internal fun build(dispatcher: CoroutineContext): EventContext = EventContext(sources, filters, dispatcher) -} \ No newline at end of file +} diff --git a/autohelp/src/main/kotlin/me/schlaubi/autohelp/help/OutgoingMessage.kt b/autohelp/src/main/kotlin/me/schlaubi/autohelp/help/OutgoingMessage.kt index 9bbab21..4b6c9b9 100644 --- a/autohelp/src/main/kotlin/me/schlaubi/autohelp/help/OutgoingMessage.kt +++ b/autohelp/src/main/kotlin/me/schlaubi/autohelp/help/OutgoingMessage.kt @@ -56,8 +56,12 @@ public data class Embed( public data class Footer(public val name: String, public val url: String?, public val avatar: String?) } +@Suppress("LongMethod") @OptIn(ExperimentalStdlibApi::class) -internal fun DiscordConversation.toEmbed(htmlRenderer: HtmlRenderer, loadingEmote: String): Embed { +internal fun DiscordConversation.toEmbed( + htmlRenderer: HtmlRenderer, + loadingEmote: String +): Embed { val fields = buildList { val stackTrace = exception if (stackTrace != null) { @@ -110,7 +114,8 @@ internal fun DiscordConversation.toEmbed(htmlRenderer: HtmlRenderer, loadingEmot add( Embed.Field( "Ursache - Code", - "Ich konnte keinen Code finden, bitte schicke die komplette Klasse, in der der Fehler auftritt (Am besten via hastebin)", + "Ich konnte keinen Code finden, bitte schicke die komplette" + + " Klasse, in der der Fehler auftritt (Am besten via hastebin)", false ) ) diff --git a/autohelp/src/main/kotlin/me/schlaubi/autohelp/internal/AutoHelpImpl.kt b/autohelp/src/main/kotlin/me/schlaubi/autohelp/internal/AutoHelpImpl.kt index cce087e..bbc90d7 100644 --- a/autohelp/src/main/kotlin/me/schlaubi/autohelp/internal/AutoHelpImpl.kt +++ b/autohelp/src/main/kotlin/me/schlaubi/autohelp/internal/AutoHelpImpl.kt @@ -39,6 +39,7 @@ import me.schlaubi.autohelp.tags.TagSupplier import kotlin.coroutines.CoroutineContext import kotlin.time.Duration +@Suppress("LongParameterList") internal class AutoHelpImpl( override val analyzer: StackTraceAnalyzer, contexts: List>, override val coroutineContext: CoroutineContext, diff --git a/autohelp/src/main/kotlin/me/schlaubi/autohelp/internal/EventHandler.kt b/autohelp/src/main/kotlin/me/schlaubi/autohelp/internal/EventHandler.kt index cea1365..26f32f3 100644 --- a/autohelp/src/main/kotlin/me/schlaubi/autohelp/internal/EventHandler.kt +++ b/autohelp/src/main/kotlin/me/schlaubi/autohelp/internal/EventHandler.kt @@ -71,6 +71,7 @@ internal suspend fun ReceivedMessage.handle(autoHelp: AutoHelpImpl) { } } + @Suppress("MagicNumber") if (found) { autoHelp.launch { withTimeout(Duration.ofSeconds(10)) { diff --git a/autohelp/src/main/kotlin/me/schlaubi/autohelp/source/EventContext.kt b/autohelp/src/main/kotlin/me/schlaubi/autohelp/source/EventContext.kt index d7f85ec..4926807 100644 --- a/autohelp/src/main/kotlin/me/schlaubi/autohelp/source/EventContext.kt +++ b/autohelp/src/main/kotlin/me/schlaubi/autohelp/source/EventContext.kt @@ -49,4 +49,4 @@ public class EventContext( .filter { event -> filters.all { it.validateEvent(event) } } -} \ No newline at end of file +} diff --git a/autohelp/src/main/kotlin/me/schlaubi/autohelp/tags/TagSupplier.kt b/autohelp/src/main/kotlin/me/schlaubi/autohelp/tags/TagSupplier.kt index 3adc300..eda61b7 100644 --- a/autohelp/src/main/kotlin/me/schlaubi/autohelp/tags/TagSupplier.kt +++ b/autohelp/src/main/kotlin/me/schlaubi/autohelp/tags/TagSupplier.kt @@ -35,4 +35,4 @@ public fun interface TagSupplier { * Finds a tag that explains [exception] or `null` if none was found. */ public fun findTagForException(exception: StackTrace): String? -} \ No newline at end of file +} diff --git a/build.gradle.kts b/build.gradle.kts index 27e0000..63cddbe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,22 +36,21 @@ import io.gitlab.arturbosch.detekt.Detekt import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.5.0" - kotlin("kapt") version "1.5.0" - kotlin("plugin.serialization") version "1.5.0" - id("io.gitlab.arturbosch.detekt") version "1.15.0" + kotlin("jvm") version "1.5.21" + kotlin("kapt") version "1.5.21" + kotlin("plugin.serialization") version "1.5.21" + id("io.gitlab.arturbosch.detekt") version "1.17.1" application antlr } group = "de.nycode" -version = "1.2.0-hotifx.1" +version = "1.3.0" repositories { mavenCentral() maven("https://jitpack.io") maven("https://oss.sonatype.org/content/repositories/snapshots") - maven("https://kotlin.bintray.com/kotlinx/") maven("https://schlaubi.jfrog.io/artifactory/lavakord") maven("https://schlaubi.jfrog.io/artifactory/forp") } @@ -62,49 +61,40 @@ application { dependencies { runtimeOnly(kotlin("scripting-jsr223")) - implementation("org.jetbrains.kotlinx", "kotlinx-datetime", "0.1.1") - implementation("org.jetbrains.kotlinx", "kotlinx-serialization-json", "1.0.0") { - version { - strictly("1.0.0") - } - } + implementation("org.jetbrains.kotlinx", "kotlinx-datetime", "0.2.1") + implementation("org.jetbrains.kotlinx", "kotlinx-serialization-json", "1.2.1") - implementation("dev.kord", "kord-core", "kotlin-1.5-SNAPSHOT") { - version { - strictly("kotlin-1.5-SNAPSHOT") - } - } - implementation("dev.kord.x", "emoji", "0.5.0-SNAPSHOT") + implementation("dev.kord", "kord-core", "0.7.3") + implementation("dev.kord.x", "emoji", "0.5.0") implementation("dev.kord.x", "commands-runtime-kord", "0.4.0-SNAPSHOT") kapt("dev.kord.x", "commands-processor", "0.4.0-SNAPSHOT") - val ktorVersion = "1.4.1" - implementation("io.ktor", "ktor-client", ktorVersion) - implementation("io.ktor", "ktor-client-cio", ktorVersion) - implementation("io.ktor", "ktor-client-json", ktorVersion) - implementation("io.ktor", "ktor-serialization", ktorVersion) + implementation(platform("io.ktor:ktor-bom:1.6.1")) + implementation("io.ktor", "ktor-client") + implementation("io.ktor", "ktor-client-cio") + implementation("io.ktor", "ktor-client-json") + implementation("io.ktor", "ktor-serialization") + implementation("io.ktor", "ktor-server-core") + implementation("io.ktor", "ktor-server-cio") - implementation("io.ktor", "ktor-server-core", ktorVersion) - implementation("io.ktor", "ktor-server-cio", ktorVersion) - - implementation("io.github.microutils", "kotlin-logging", "1.12.0") + implementation("io.github.microutils", "kotlin-logging", "2.0.10") implementation("io.github.cdimascio", "dotenv-kotlin", "6.2.2") - implementation("org.litote.kmongo", "kmongo-coroutine-serialization", "4.2.3") + implementation("org.litote.kmongo", "kmongo-coroutine-serialization", "4.2.8") implementation("ch.qos.logback", "logback-classic", "1.2.3") - implementation("io.sentry", "sentry", "3.1.0") - implementation("io.sentry", "sentry-logback", "3.2.0") + implementation("io.sentry", "sentry", "5.0.1") + implementation("io.sentry", "sentry-logback", "5.0.1") - implementation("com.vladsch.flexmark", "flexmark-html2md-converter", "0.60.2") + implementation("com.vladsch.flexmark", "flexmark-html2md-converter", "0.62.2") - implementation("dev.schlaubi.lavakord", "kord", "1.0.0-SNAPSHOT") + implementation("dev.schlaubi.lavakord", "kord", "2.0.0") - implementation("org.ow2.asm", "asm", "9.1") - implementation("org.ow2.asm", "asm-tree", "9.1") + implementation("org.ow2.asm", "asm", "9.2") + implementation("org.ow2.asm", "asm-tree", "9.2") - detektPlugins("io.gitlab.arturbosch.detekt", "detekt-formatting", "1.15.0") + detektPlugins("io.gitlab.arturbosch.detekt", "detekt-formatting", "1.17.1") - antlr("org.antlr", "antlr4", "4.9.1") + antlr("org.antlr", "antlr4", "4.9.2") implementation(project(":autohelp:kord")) implementation("dev.schlaubi.forp", "forp-analyze-client", "1.0-SNAPSHOT") @@ -113,8 +103,8 @@ dependencies { testImplementation(kotlin("test-junit5")) testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine", "5.6.0") testRuntimeOnly("org.slf4j", "slf4j-simple", "1.7.30") - testImplementation("org.jetbrains.kotlinx", "kotlinx-coroutines-test", "1.4.2") - testImplementation("com.willowtreeapps.assertk", "assertk-jvm", "0.23") + testImplementation("org.jetbrains.kotlinx", "kotlinx-coroutines-test", "1.5.1") + testImplementation("com.willowtreeapps.assertk", "assertk-jvm", "0.24") } // Kotlin dsl @@ -126,11 +116,6 @@ tasks { } } - withType { - // Target version of the generated JVM bytecode. It is used for type resolution. - this.jvmTarget = "1.8" - } - generateGrammarSource { outputDirectory = File("${project.buildDir}/generated-src/antlr/main/de/nycode/bankobot/variables") @@ -141,3 +126,20 @@ tasks { useJUnitPlatform() } } + +detekt { + autoCorrect = true +} + +subprojects { + apply(plugin = "io.gitlab.arturbosch.detekt") + + tasks { + withType().configureEach { + // Target version of the generated JVM bytecode. It is used for type resolution. + this.jvmTarget = "15" + + autoCorrect = true + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0cf89e6..23c6738 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -35,6 +35,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/kotlin/de/nycode/bankobot/BankoBot.kt b/src/main/kotlin/de/nycode/bankobot/BankoBot.kt index e22ec29..0fcb2fa 100644 --- a/src/main/kotlin/de/nycode/bankobot/BankoBot.kt +++ b/src/main/kotlin/de/nycode/bankobot/BankoBot.kt @@ -72,7 +72,6 @@ import io.ktor.client.engine.cio.* import io.ktor.client.features.* import io.ktor.client.features.json.* import io.ktor.client.features.json.serializer.* -import io.ktor.util.* import kapt.kotlin.generated.configure import kotlinx.coroutines.* import kotlinx.coroutines.flow.toList @@ -88,7 +87,6 @@ import org.litote.kmongo.serialization.registerSerializer import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.ExperimentalTime -import kotlin.time.seconds object BankoBot : CoroutineScope { @@ -101,7 +99,7 @@ object BankoBot : CoroutineScope { private val logger = KotlinLogging.logger { } @Suppress("MagicNumber") - @OptIn(KtorExperimentalAPI::class, ExperimentalTime::class) + @OptIn(ExperimentalTime::class) val httpClient = HttpClient(CIO) { install(JsonFeature) { val json = kotlinx.serialization.json.Json { diff --git a/src/main/kotlin/de/nycode/bankobot/command/BankoBotContextConverter.kt b/src/main/kotlin/de/nycode/bankobot/command/BankoBotContextConverter.kt index f7fcdcc..39ab02a 100644 --- a/src/main/kotlin/de/nycode/bankobot/command/BankoBotContextConverter.kt +++ b/src/main/kotlin/de/nycode/bankobot/command/BankoBotContextConverter.kt @@ -49,12 +49,13 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import kotlin.time.Duration import kotlin.time.ExperimentalTime import kotlin.time.seconds @Suppress("MagicNumber") @OptIn(ExperimentalTime::class) -private val timeout = 5.seconds +private val timeout = Duration.seconds(5) /** * Implementation of [ContextConverter] which sends typing before replying to a command. diff --git a/src/main/kotlin/de/nycode/bankobot/command/Context.kt b/src/main/kotlin/de/nycode/bankobot/command/Context.kt index cfd2079..ca9beb3 100644 --- a/src/main/kotlin/de/nycode/bankobot/command/Context.kt +++ b/src/main/kotlin/de/nycode/bankobot/command/Context.kt @@ -95,7 +95,7 @@ interface EditableMessage { fun KordCommandEvent.toContext(): Context = DelegatedKordCommandContext(this) -private class DelegatedKordCommandContext(private val delegate: KordCommandEvent) : Context, KordEvent by delegate { +internal class DelegatedKordCommandContext(private val delegate: KordCommandEvent) : Context, KordEvent by delegate { override val command: Command<*> get() = delegate.command override val commands: Map> get() = delegate.commands override val processor: CommandProcessor get() = delegate.processor diff --git a/src/main/kotlin/de/nycode/bankobot/command/ErrorHandlers.kt b/src/main/kotlin/de/nycode/bankobot/command/ErrorHandlers.kt index 42d68b7..d412536 100644 --- a/src/main/kotlin/de/nycode/bankobot/command/ErrorHandlers.kt +++ b/src/main/kotlin/de/nycode/bankobot/command/ErrorHandlers.kt @@ -59,14 +59,16 @@ private val kordHandler = KordErrorHandler() const val ERROR_MARKER = "[ERROR]" +private typealias ErrorHandlerInterface = ErrorHandler + +private typealias RejectedArgument = ErrorHandler.RejectedArgument + @Suppress("UnnecessaryAbstractClass", "UNCHECKED_CAST") abstract class AbstractErrorHandler : - ErrorHandler { - override suspend fun CommandProcessor.rejectArgument( - rejection: ErrorHandler.RejectedArgument, - ) { + ErrorHandlerInterface { + override suspend fun CommandProcessor.rejectArgument(rejection: RejectedArgument) { if (rejection.message == "Expected more input but reached end.") { rejection.event.message.channel.createEmbed(Embeds.command(rejection.command, this)) } else with(kordHandler) { diff --git a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/Creator.kt b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/Creator.kt index e3ae165..c9afc93 100644 --- a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/Creator.kt +++ b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/Creator.kt @@ -40,6 +40,8 @@ import dev.kord.rest.builder.interaction.ApplicationCommandCreateBuilder import dev.kord.rest.builder.interaction.ApplicationCommandsCreateBuilder import dev.kord.x.commands.model.command.CommandBuilder +private val NAME_REGEX = "^[\\w-]{1,32}\$".toRegex() + /** * Class containing all necessary information to register a slash command * @property name the name @@ -61,6 +63,8 @@ class SlashCommandCreator( @OptIn(KordPreview::class) fun CommandBuilder<*, *, *>.toSlashCommand(): SlashCommandCreator { val description = description ?: "" + require(name.matches(NAME_REGEX)) { "$name is not a valid name" } + require(name.none(Char::isUpperCase)) { "$name has upper case characters" } val creator: ApplicationCommandCreateBuilder.() -> Unit = { arguments.forEach { if (it is SlashArgument<*, *>) { diff --git a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/InteractionEventHandler.kt b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/InteractionEventHandler.kt index 146b0c9..42a7eaa 100644 --- a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/InteractionEventHandler.kt +++ b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/InteractionEventHandler.kt @@ -53,10 +53,7 @@ import dev.kord.core.entity.Message import dev.kord.core.entity.Role import dev.kord.core.entity.User import dev.kord.core.entity.channel.TextChannel -import dev.kord.core.entity.interaction.GuildInteraction -import dev.kord.core.entity.interaction.Interaction -import dev.kord.core.entity.interaction.InteractionCommand -import dev.kord.core.entity.interaction.OptionValue +import dev.kord.core.entity.interaction.* import dev.kord.core.event.Event import dev.kord.core.event.interaction.InteractionCreateEvent import dev.kord.core.event.message.MessageCreateEvent @@ -67,6 +64,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.datetime.Clock import kotlinx.datetime.toJavaInstant import mu.KotlinLogging +import java.util.* import kotlin.time.ExperimentalTime private val LOG = KotlinLogging.logger {} @@ -84,6 +82,7 @@ object InteractionEventHandler : EventHandler { override val context: ProcessorContext get() = InteractionContext + @Suppress("LongMethod") @OptIn(ExperimentalTime::class) override suspend fun CommandProcessor.onEvent(event: InteractionCreateEvent) { val guildInteraction = event.interaction as? GuildInteraction ?: return @@ -92,7 +91,7 @@ object InteractionEventHandler : EventHandler { val commandName = guildInteraction.command.rootName val foundCommand = getCommand(context, commandName) val ephemeral = foundCommand?.ephemeral ?: false - val ack = if (ephemeral) guildInteraction.acknowledgeEphemeral() else guildInteraction.ackowledgePublic() + val ack = if (ephemeral) guildInteraction.acknowledgeEphemeral() else guildInteraction.acknowledgePublic() val messageCreate by lazy { event.toMessageCreateEvent(guildInteraction) } val commandEvent by lazy { if (ephemeral) { @@ -101,7 +100,12 @@ object InteractionEventHandler : EventHandler { ) } else { SlashCommandContext( - messageCreate, foundCommand, commands, this, ack as PublicInteractionResponseBehavior + messageCreate, + foundCommand, + commands, + this, + guildInteraction.id, + ack as PublicInteractionResponseBehavior ) } } @@ -160,7 +164,7 @@ object InteractionEventHandler : EventHandler { val items = mutableListOf() var indexTrim = 2 + command.rootName.length // / arguments.forEachIndexed { index, argument -> - val option = command.options[argument.name.toLowerCase()] + val option = command.options[argument.name.lowercase(Locale.getDefault())] // each argument is prefix by " :" indexTrim += argument.name.length + 2 val argumentValue = @@ -220,7 +224,7 @@ object InteractionEventHandler : EventHandler { } @OptIn(KordPreview::class) -private fun Interaction.buildInvokeString(): String { +private fun CommandInteraction.buildInvokeString(): String { val builder = StringBuilder("/") builder.append(command.rootName) // command diff --git a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/Pipeline.kt b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/Pipeline.kt index 72584fc..8171c6d 100644 --- a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/Pipeline.kt +++ b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/Pipeline.kt @@ -72,10 +72,10 @@ object KordInteractionErrorHandler : private const val backtickEscape = "\u200E`" override suspend fun CommandProcessor.rejectArgument( - rejection: ErrorHandler.RejectedArgument, - ) { + rejection: ErrorHandler.RejectedArgument, +) { with(rejection) { respondError( event, diff --git a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/SlashArgument.kt b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/SlashArgument.kt index 05bb6b6..b0f1bd4 100644 --- a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/SlashArgument.kt +++ b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/SlashArgument.kt @@ -38,6 +38,7 @@ import dev.kord.common.annotation.KordPreview import dev.kord.rest.builder.interaction.BaseApplicationBuilder import dev.kord.x.commands.argument.Argument import de.nycode.bankobot.command.slashcommands.arguments.AbstractSlashCommandArgument +import java.util.* /** * [Argument] which is compatible with slash commands. @@ -52,6 +53,8 @@ import de.nycode.bankobot.command.slashcommands.arguments.AbstractSlashCommandAr interface SlashArgument : Argument { val description: String val required: Boolean + override val name: String + get() = name.lowercase(Locale.getDefault()) /** * Applies the argument configuration to the [BaseApplicationBuilder] diff --git a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/SlashCommandContext.kt b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/SlashCommandContext.kt index 262f61f..762434f 100644 --- a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/SlashCommandContext.kt +++ b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/SlashCommandContext.kt @@ -39,11 +39,9 @@ import de.nycode.bankobot.command.Context import de.nycode.bankobot.command.EditableMessage import dev.kord.common.annotation.KordPreview import dev.kord.common.annotation.KordUnsafe -import dev.kord.core.behavior.interaction.EphemeralInteractionResponseBehavior -import dev.kord.core.behavior.interaction.PublicInteractionResponseBehavior -import dev.kord.core.behavior.interaction.edit -import dev.kord.core.behavior.interaction.followUp -import dev.kord.core.entity.interaction.PublicFollowupMessage +import dev.kord.common.entity.Snowflake +import dev.kord.core.behavior.interaction.* +import dev.kord.core.entity.interaction.EphemeralFollowupMessage import dev.kord.core.event.message.MessageCreateEvent import dev.kord.rest.builder.message.MessageCreateBuilder import dev.kord.rest.builder.message.MessageModifyBuilder @@ -64,17 +62,17 @@ class EphemeralSlashCommandContext( @OptIn(KordUnsafe::class) override suspend fun createResponse(builder: suspend MessageCreateBuilder.() -> Unit): EditableMessage { val messageBuilder = MessageCreateBuilder().apply { builder() } - val response = ack.followUp { + val response = ack.followUpEphemeral { content = messageBuilder.content - embeds = messageBuilder.embed?.let { mutableListOf(it.toRequest()) } ?: mutableListOf() - allowedMentions = messageBuilder.allowedMentions?.build() + messageBuilder.embed?.let { embeds.add(it) } + allowedMentions = messageBuilder.allowedMentions } return EditableEphemeralFollowUp(response) } } -class EditableEphemeralFollowUp(private val response: PublicFollowupMessage) : EditableMessage { +class EditableEphemeralFollowUp(private val response: EphemeralFollowupMessage) : EditableMessage { override suspend fun modify(builder: suspend MessageModifyBuilder.() -> Unit): EditableMessage { val messageBuilder = MessageModifyBuilder().apply { builder() } response.edit { @@ -85,7 +83,7 @@ class EditableEphemeralFollowUp(private val response: PublicFollowupMessage) : E return this } - override suspend fun delete() = response.delete() + override suspend fun delete() = throw UnsupportedOperationException("Not supported by this type of message") } class SlashCommandContext( @@ -93,7 +91,8 @@ class SlashCommandContext( private val _command: Command<*>?, override val commands: Map>, override val processor: CommandProcessor, - private val ack: PublicInteractionResponseBehavior + internal val interactionId: Snowflake, + internal val ack: PublicInteractionResponseBehavior ) : Context { override val command: Command<*> @@ -107,8 +106,8 @@ class SlashCommandContext( return if (responded) { ack.followUp { content = messageBuilder.content - embeds = messageBuilder.embed?.let { mutableListOf(it.toRequest()) } ?: mutableListOf() - allowedMentions = messageBuilder.allowedMentions?.build() + messageBuilder.embed?.let { embeds.add(it) } + allowedMentions = messageBuilder.allowedMentions } responded = true @@ -136,6 +135,7 @@ private class EditableAck(private val ack: PublicInteractionResponseBehavior) : return this } + @OptIn(KordPreview::class) override suspend fun delete() = ack.delete() } diff --git a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/arguments/AbstractSlashCommandArgument.kt b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/arguments/AbstractSlashCommandArgument.kt index 8b966b5..3530754 100644 --- a/src/main/kotlin/de/nycode/bankobot/command/slashcommands/arguments/AbstractSlashCommandArgument.kt +++ b/src/main/kotlin/de/nycode/bankobot/command/slashcommands/arguments/AbstractSlashCommandArgument.kt @@ -39,6 +39,7 @@ import dev.kord.common.annotation.KordPreview import dev.kord.rest.builder.interaction.OptionsBuilder import dev.kord.x.commands.argument.Argument import dev.kord.x.commands.argument.extension.optional +import java.util.* /** * Abstract implementation for [SlashArgument] which delegates [Argument] to [delegate] @@ -48,6 +49,9 @@ abstract class AbstractSlashCommandArgument( override val description: String, private val delegate: Argument, ) : Argument by delegate, SlashArgument { + override val name: String + get() = delegate.name.lowercase(Locale.getDefault()) + override val required: Boolean get() = !delegate.toString().contains("optional|default".toRegex()) diff --git a/src/main/kotlin/de/nycode/bankobot/commands/TestCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/TestCommand.kt index f4baf60..fa56fe5 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/TestCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/TestCommand.kt @@ -37,7 +37,7 @@ package de.nycode.bankobot.commands import de.nycode.bankobot.command.command import de.nycode.bankobot.command.permissions.PermissionLevel import de.nycode.bankobot.command.permissions.permission -import de.nycode.bankobot.utils.paginate +import de.nycode.bankobot.utils.paginator.paginate import dev.kord.x.commands.annotation.AutoWired import dev.kord.x.commands.annotation.ModuleName import dev.kord.x.commands.model.command.invoke diff --git a/src/main/kotlin/de/nycode/bankobot/commands/dev/EvalCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/dev/EvalCommand.kt index 9b16db3..65190ad 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/dev/EvalCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/dev/EvalCommand.kt @@ -132,7 +132,7 @@ fun evalCommand() = command("ev") { @OptIn(ExperimentalTime::class) private suspend fun eval(engine: ScriptEngine, code: String): Any? { - return withTimeout(1.minutes) { + return withTimeout(Duration.minutes(1)) { try { //language=kotlin engine.eval( diff --git a/src/main/kotlin/de/nycode/bankobot/commands/general/DocsCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/general/DocsCommand.kt index fb2dbed..ebaddec 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/general/DocsCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/general/DocsCommand.kt @@ -56,6 +56,7 @@ import dev.kord.x.commands.argument.extension.named import dev.kord.x.commands.argument.result.extension.FilterResult import dev.kord.x.commands.argument.text.WordArgument import dev.kord.x.commands.model.command.invoke +import java.util.* @Suppress("TopLevelPropertyNaming") const val DocsModule = "Documentation" @@ -77,7 +78,7 @@ private fun Argument.docsFilter() = filter { doc -> @OptIn(KordPreview::class) private val JavaDocArgument = WordArgument .named("javadoc-name") - .map { it.toLowerCase() } + .map { it.lowercase(Locale.getDefault()) } .docsFilter() .asSlashArgument("Das Javadoc in dem gesucht werden soll") { choice("JDK 11 Dokumentation", "jdk11") @@ -176,7 +177,7 @@ private fun formatClassDefinition(doc: DocumentedClassObject): String = " ", postfix = "\n" ) { "@${it.className}" } - }${doc.modifiers.joinToString(" ")} ${doc.type.name.toLowerCase()} ${doc.name} ${ + }${doc.modifiers.joinToString(" ")} ${doc.type.name.lowercase(Locale.getDefault())} ${doc.name} ${ if (doc.metadata.extensions.isNotEmpty()) { "extends ${doc.metadata.extensions.first().className} " } else { diff --git a/src/main/kotlin/de/nycode/bankobot/commands/general/JDoodleCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/general/JDoodleCommand.kt index 6930f1a..1290e68 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/general/JDoodleCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/general/JDoodleCommand.kt @@ -52,6 +52,7 @@ import dev.kord.x.commands.kord.argument.CodeBlock import dev.kord.x.commands.kord.argument.CodeBlockArgument import dev.kord.x.commands.model.command.invoke import dev.kord.x.emoji.Emojis +import kotlin.time.Duration import kotlin.time.ExperimentalTime import kotlin.time.seconds @@ -99,7 +100,7 @@ private suspend fun Context.executeViaJDoodle(argument: CodeBlock) { editEmbed(Embeds.error("Heute leider nicht!", descriptionString)) } else -> { - val cpuTime = response.cpuTime?.seconds + val cpuTime = response.cpuTime?.let { Duration.seconds(it) } val memory = response.memory?.let { it / 1000.0 } val descriptionString = "```${Emojis.timerClock} $cpuTime " + "${Emojis.desktopComputer} $memory KB```" + diff --git a/src/main/kotlin/de/nycode/bankobot/commands/general/SearchCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/general/SearchCommand.kt index a297a2c..b18b9ec 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/general/SearchCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/general/SearchCommand.kt @@ -40,10 +40,9 @@ import de.nycode.bankobot.command.description import de.nycode.bankobot.command.slashcommands.arguments.asSlashArgument import de.nycode.bankobot.commands.GeneralModule import de.nycode.bankobot.utils.Embeds -import de.nycode.bankobot.utils.Embeds.editEmbed import de.nycode.bankobot.utils.GoogleUtil import de.nycode.bankobot.utils.doExpensiveTask -import de.nycode.bankobot.utils.paginate +import de.nycode.bankobot.utils.paginator.paginate import dev.kord.x.commands.annotation.AutoWired import dev.kord.x.commands.annotation.ModuleName import dev.kord.x.commands.argument.extension.named @@ -70,7 +69,7 @@ private suspend fun Context.search(search: String) { editEmbed(Embeds.error("Schade!", "Google möchte dir anscheinend nicht antworten! ._.")) } else { delete() - list.paginate(channel, "Suchergebnisse") { + list.paginate(this@search, "Suchergebnisse") { itemsPerPage = 1 } } diff --git a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/ListTagsCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/ListTagsCommand.kt index dda5d46..90d65b9 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/ListTagsCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/ListTagsCommand.kt @@ -39,12 +39,13 @@ import de.nycode.bankobot.command.command import de.nycode.bankobot.command.description import de.nycode.bankobot.command.slashcommands.arguments.asSlashArgument import de.nycode.bankobot.commands.TagModule -import de.nycode.bankobot.utils.LazyItemProvider -import de.nycode.bankobot.utils.paginate +import de.nycode.bankobot.utils.paginator.LazyItemProvider +import de.nycode.bankobot.utils.paginator.paginate import dev.kord.x.commands.annotation.AutoWired import dev.kord.x.commands.annotation.ModuleName import dev.kord.x.commands.argument.extension.named import dev.kord.x.commands.argument.extension.optional +import dev.kord.x.commands.argument.extension.strictPositive import dev.kord.x.commands.argument.primitive.IntArgument import dev.kord.x.commands.model.command.invoke import dev.kord.x.commands.model.module.CommandSet @@ -57,8 +58,9 @@ internal fun listTagsCommand(): CommandSet = command("list-tags") { description("Alle Tags anzeigen") invoke( - IntArgument.named("page").optional(1).asSlashArgument("Die Seite"), - IntArgument.named("pageSize").optional(8).asSlashArgument("Die Seitengröße") + IntArgument.strictPositive().named("page").optional(1).asSlashArgument("Die Seite"), + IntArgument.strictPositive().named("pageSize").optional(8) + .asSlashArgument("Die Seitengröße") ) { page, pageSize -> val tagCount = BankoBot.repositories.tag.countDocuments().toInt() @@ -67,7 +69,7 @@ internal fun listTagsCommand(): CommandSet = command("list-tags") { .paginate(start, pageSize) { it.name } - }.paginate(message.channel, "Tags") { + }.paginate(this, "Tags") { firstPage = page itemsPerPage = pageSize } diff --git a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/SearchTagsCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/SearchTagsCommand.kt index 434a47d..25b9534 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/SearchTagsCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/SearchTagsCommand.kt @@ -39,8 +39,8 @@ import de.nycode.bankobot.command.slashcommands.arguments.asSlashArgument import de.nycode.bankobot.commands.TagModule import de.nycode.bankobot.commands.tag.searchTags import de.nycode.bankobot.utils.Embeds -import de.nycode.bankobot.utils.LazyItemProvider -import de.nycode.bankobot.utils.paginate +import de.nycode.bankobot.utils.paginator.LazyItemProvider +import de.nycode.bankobot.utils.paginator.paginate import dev.kord.x.commands.annotation.AutoWired import dev.kord.x.commands.annotation.ModuleName import dev.kord.x.commands.argument.extension.named @@ -68,6 +68,6 @@ internal fun searchTagsCommand(): CommandSet = command("search-tag") { LazyItemProvider(tags.size) { start, end -> tags.subList(start, end + 1) .map { it.name } - }.paginate(message.channel, "Tag-Suche \"$search\"") + }.paginate(this, "Tag-Suche \"$search\"") } } diff --git a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TagCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TagCommand.kt index 9680d2a..1480180 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TagCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TagCommand.kt @@ -37,6 +37,7 @@ package de.nycode.bankobot.commands.tag.commands import de.nycode.bankobot.BankoBot import de.nycode.bankobot.command.command import de.nycode.bankobot.command.description +import de.nycode.bankobot.command.slashcommands.arguments.asSlashArgument import de.nycode.bankobot.commands.TagModule import de.nycode.bankobot.commands.tag.TagArgument import de.nycode.bankobot.commands.tag.TagEntry @@ -44,12 +45,18 @@ import de.nycode.bankobot.commands.tag.UseAction import de.nycode.bankobot.commands.tag.checkEmpty import dev.kord.x.commands.annotation.AutoWired import dev.kord.x.commands.annotation.ModuleName +import dev.kord.x.commands.argument.extension.optional +import dev.kord.x.commands.kord.argument.UserArgument import dev.kord.x.commands.model.command.invoke import dev.kord.x.commands.model.module.CommandSet import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime +private val TargetArgument = UserArgument + .optional() + .asSlashArgument("Der Nutzer an den diese Antwort gerichtet ist") + @PublishedApi @AutoWired @ModuleName(TagModule) @@ -57,15 +64,24 @@ internal fun tagCommand(): CommandSet = command("tag") { alias("t") description("Einen Tag anzeigen") - invoke(TagArgument) { tag -> + invoke(TagArgument, TargetArgument) { tag, user -> if (checkEmpty(tag)) { return@invoke } tag as TagEntry - sendResponse(tag.text) { - allowedMentions {} + sendResponse("") { + content = user?.mention + embed { + description = tag.text + } + + allowedMentions { + user?.let { + users.add(it.id) + } + } } val useAction = UseAction( diff --git a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TagsFromUserCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TagsFromUserCommand.kt index 173cd85..26298b1 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TagsFromUserCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TagsFromUserCommand.kt @@ -40,8 +40,8 @@ import de.nycode.bankobot.command.slashcommands.arguments.asSlashArgument import de.nycode.bankobot.commands.TagModule import de.nycode.bankobot.commands.tag.TagEntry import de.nycode.bankobot.utils.Embeds -import de.nycode.bankobot.utils.LazyItemProvider -import de.nycode.bankobot.utils.paginate +import de.nycode.bankobot.utils.paginator.LazyItemProvider +import de.nycode.bankobot.utils.paginator.paginate import dev.kord.x.commands.annotation.AutoWired import dev.kord.x.commands.annotation.ModuleName import dev.kord.x.commands.argument.extension.named @@ -76,7 +76,7 @@ internal fun tagsFromUserCommand(): CommandSet = command("from-user") { .paginate(start, pageSize) { it.name } - }.paginate(message.channel, "Tags von ${member.displayName}") { + }.paginate(this, "Tags von ${member.displayName}") { itemsPerPage = pageSize } } diff --git a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TransferTagCommand.kt b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TransferTagCommand.kt index 4fd9915..64aaab6 100644 --- a/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TransferTagCommand.kt +++ b/src/main/kotlin/de/nycode/bankobot/commands/tag/commands/TransferTagCommand.kt @@ -62,6 +62,7 @@ import kotlinx.coroutines.flow.single import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout +import kotlin.time.Duration import kotlin.time.ExperimentalTime import kotlin.time.seconds @@ -127,7 +128,7 @@ internal fun transferTagCommand(): CommandSet = command("transfer") { addReaction(Emojis.whiteCheckMark) addReaction(Emojis.x) try { - withTimeout(45.seconds) { + withTimeout(Duration.seconds(45)) { val reactionEvent = live().events.filterIsInstance() .filter { it.user.id == member.id } .filter { it.emoji.name in arrayOf(Emojis.x.unicode, Emojis.whiteCheckMark.unicode) } diff --git a/src/main/kotlin/de/nycode/bankobot/docdex/DocsGoogle.kt b/src/main/kotlin/de/nycode/bankobot/docdex/DocsGoogle.kt index 6222cee..ef18eae 100644 --- a/src/main/kotlin/de/nycode/bankobot/docdex/DocsGoogle.kt +++ b/src/main/kotlin/de/nycode/bankobot/docdex/DocsGoogle.kt @@ -35,6 +35,7 @@ package de.nycode.bankobot.docdex import info.debatty.java.stringsimilarity.Levenshtein +import java.util.* /** * Discount Google (aka. Bing) which searches for docs @@ -57,15 +58,21 @@ object DocsGoogle { if (query.`package` != null) { penalty += levenshtein.distance( - query.`package`.toLowerCase(), - obj.`package`.toLowerCase() + query.`package`.lowercase(Locale.getDefault()), + obj.`package`.lowercase(Locale.getDefault()) ) } if (query.clazz != null) { penalty += levenshtein.distance( - query.clazz.toLowerCase(), - if (obj is DocumentedMethodObject) obj.metadata.owner.toLowerCase() else obj.name.toLowerCase() + query.clazz.lowercase(Locale.getDefault()), + if (obj is DocumentedMethodObject) { + obj.metadata.owner.lowercase(Locale.getDefault()) + } else { + obj.name.lowercase( + Locale.getDefault() + ) + } ) * 0.3 // this has a very low value since sometimes you refer // to a method from an interface by a common implementation @@ -77,8 +84,8 @@ object DocsGoogle { penalty += 100 } else { penalty += levenshtein.distance( - query.method.toLowerCase(), - obj.name.toLowerCase() + query.method.lowercase(Locale.getDefault()), + obj.name.lowercase(Locale.getDefault()) ) * 20 // This has a very high value, see explanation for className above } } diff --git a/src/main/kotlin/de/nycode/bankobot/twitch/TwitchIntegration.kt b/src/main/kotlin/de/nycode/bankobot/twitch/TwitchIntegration.kt index 9ee8a6a..d1494dd 100644 --- a/src/main/kotlin/de/nycode/bankobot/twitch/TwitchIntegration.kt +++ b/src/main/kotlin/de/nycode/bankobot/twitch/TwitchIntegration.kt @@ -42,10 +42,7 @@ import dev.kord.rest.request.errorString import io.ktor.http.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.ticker -import kotlin.time.ExperimentalTime -import kotlin.time.days -import kotlin.time.milliseconds -import kotlin.time.seconds +import kotlin.time.* const val TOKEN_URL = "https://id.twitch.tv/oauth2/token" @@ -73,7 +70,7 @@ internal fun Kord.twitchIntegration() = this.launch { internal suspend fun Kord.updatePresence(stream: TwitchStream) { if (stream.isLive) { editPresence { - streaming(if (stream.title.isBlank()) "Banko auf Twitch" else stream.title, "https://twitch.tv/DerBanko") + streaming(stream.title.ifBlank { "Banko auf Twitch" }, "https://twitch.tv/DerBanko") } } else { editPresence { @@ -114,9 +111,9 @@ private fun CoroutineScope.launchSubscriptionUpdater( token: TwitchAccessTokenResponse ) = launch { val duration = if (Config.ENVIRONMENT == Environment.PRODUCTION) { - 1.days + Duration.days(1) } else { - 30.seconds + Duration.seconds(30) } val delay = duration.inWholeMilliseconds for (unit in ticker(delayMillis = delay, initialDelayMillis = delay)) { @@ -124,7 +121,7 @@ private fun CoroutineScope.launchSubscriptionUpdater( userId, "subscribe", token, - duration = delay.milliseconds.inSeconds.toInt() + duration = Duration.milliseconds(delay).toInt(DurationUnit.SECONDS) ) webhookLogger.info("Updated twitch webhook subscription!") } diff --git a/src/main/kotlin/de/nycode/bankobot/twitch/WebhookServer.kt b/src/main/kotlin/de/nycode/bankobot/twitch/WebhookServer.kt index 696d43a..dc180cf 100644 --- a/src/main/kotlin/de/nycode/bankobot/twitch/WebhookServer.kt +++ b/src/main/kotlin/de/nycode/bankobot/twitch/WebhookServer.kt @@ -47,7 +47,6 @@ import io.ktor.routing.* import io.ktor.serialization.* import io.ktor.server.cio.* import io.ktor.server.engine.* -import io.ktor.util.* import mu.KotlinLogging internal val webhookLogger by lazy { KotlinLogging.logger("Webhooks") } @@ -55,7 +54,6 @@ internal val webhookLogger by lazy { KotlinLogging.logger("Webhooks") } /** * Launches a ktor embedded server uses for receiving webhook notifications from the twitch api */ -@OptIn(KtorExperimentalAPI::class) internal fun Kord.launchServer() = embeddedServer(CIO) { install(Routing) { route("twitch") { diff --git a/src/main/kotlin/de/nycode/bankobot/utils/CodeBlockUtil.kt b/src/main/kotlin/de/nycode/bankobot/utils/CodeBlockUtil.kt index 93755c2..1842f4c 100644 --- a/src/main/kotlin/de/nycode/bankobot/utils/CodeBlockUtil.kt +++ b/src/main/kotlin/de/nycode/bankobot/utils/CodeBlockUtil.kt @@ -38,7 +38,7 @@ import org.intellij.lang.annotations.Language // https://regex101.com/r/euvYm9/3 @Language("RegExp") -val CODEBLOCK_REGEX = """```(?:(?:([a-zA-Z]+)\s)|\s)?((?:[\s\S])+?(?=```))```""".toRegex() +val CODEBLOCK_REGEX: Regex = """```(?:([a-zA-Z]+)\s|\s)?([\s\S]+?(?=```))```""".toRegex() fun String.toCodeBlock(): CodeBlock? { val match = CODEBLOCK_REGEX.find(this) ?: return null diff --git a/src/main/kotlin/de/nycode/bankobot/utils/Constants.kt b/src/main/kotlin/de/nycode/bankobot/utils/Constants.kt index 76ae906..93a84fc 100644 --- a/src/main/kotlin/de/nycode/bankobot/utils/Constants.kt +++ b/src/main/kotlin/de/nycode/bankobot/utils/Constants.kt @@ -37,7 +37,7 @@ package de.nycode.bankobot.utils /** * Max length for embed title. */ -const val EMBED_TITLE_MAX_LENGTH = 1024 +const val EMBED_TITLE_MAX_LENGTH: Int = 1024 -const val JDOODLE_LANGUAGE_INVALID_CODE = -100 -const val JDOODLE_CREDITS_USED_INVALID_CODE = -200 +const val JDOODLE_LANGUAGE_INVALID_CODE: Int = -100 +const val JDOODLE_CREDITS_USED_INVALID_CODE: Int = -200 diff --git a/src/main/kotlin/de/nycode/bankobot/utils/GitHubUtil.kt b/src/main/kotlin/de/nycode/bankobot/utils/GitHubUtil.kt index 22f809c..ce613ba 100644 --- a/src/main/kotlin/de/nycode/bankobot/utils/GitHubUtil.kt +++ b/src/main/kotlin/de/nycode/bankobot/utils/GitHubUtil.kt @@ -46,7 +46,7 @@ import kotlinx.serialization.Serializable object GitHubUtil { /** - * Retrieves a list of [GithubContributors][GithubContributor] of the BankoBot repository. + * Retrieves a list of [GithubContributors][GitHubContributor] of the BankoBot repository. */ suspend fun retrieveContributors(): List { return BankoBot.httpClient.get(API_BASE) { @@ -56,9 +56,9 @@ object GitHubUtil { } } - const val GITHUB_REPO = "bankobotv14/bankobot" - private val API_BASE = Url("https://api.github.com") - private const val GET_CONTRIBUTORS_ENDPOINT = "repos/$GITHUB_REPO/contributors" + const val GITHUB_REPO: String = "bankobotv14/bankobot" + private val API_BASE: Url = Url("https://api.github.com") + private const val GET_CONTRIBUTORS_ENDPOINT: String = "repos/$GITHUB_REPO/contributors" } /** diff --git a/src/main/kotlin/de/nycode/bankobot/utils/JDoodleUtil.kt b/src/main/kotlin/de/nycode/bankobot/utils/JDoodleUtil.kt index 49ea5e5..29fb6f9 100644 --- a/src/main/kotlin/de/nycode/bankobot/utils/JDoodleUtil.kt +++ b/src/main/kotlin/de/nycode/bankobot/utils/JDoodleUtil.kt @@ -119,7 +119,7 @@ object JDoodleLanguageProvider { private val VERSIONS_4 = (0..3) private val VERSIONS_5 = (0..4) - val listOfLanguages = arrayOf( + val listOfLanguages: Array = arrayOf( JDoodleLanguage("kotlin", VERSIONS_3), JDoodleLanguage("java", VERSIONS_4), JDoodleLanguage("c", VERSIONS_5), diff --git a/src/main/kotlin/de/nycode/bankobot/utils/LoadingUtil.kt b/src/main/kotlin/de/nycode/bankobot/utils/LoadingUtil.kt index 93fa0cb..b352541 100644 --- a/src/main/kotlin/de/nycode/bankobot/utils/LoadingUtil.kt +++ b/src/main/kotlin/de/nycode/bankobot/utils/LoadingUtil.kt @@ -59,7 +59,7 @@ suspend fun Context.doExpensiveTask( statusTitle: String = "Bitte warten!", statusDescription: String? = null, task: suspend EditableMessage.() -> Unit, -) = doExpensiveTask(EmbedBuilder().apply { +): Unit = doExpensiveTask(EmbedBuilder().apply { title = statusTitle description = statusDescription }, task) @@ -90,7 +90,7 @@ suspend fun MessageChannelBehavior.doExpensiveTask( statusTitle: String = "Bitte warten!", statusDescription: String? = null, task: suspend MessageBehavior.() -> Unit, -) = doExpensiveTask(Embeds.loading(statusTitle, statusDescription), task) +): Unit = doExpensiveTask(Embeds.loading(statusTitle, statusDescription), task) /** * Helper function that runs an expensive [task] in a coroutine and sends a loading message in this channel. diff --git a/src/main/kotlin/de/nycode/bankobot/utils/StringUtil.kt b/src/main/kotlin/de/nycode/bankobot/utils/StringUtil.kt index f923ad8..2faa96e 100644 --- a/src/main/kotlin/de/nycode/bankobot/utils/StringUtil.kt +++ b/src/main/kotlin/de/nycode/bankobot/utils/StringUtil.kt @@ -39,13 +39,13 @@ import java.security.MessageDigest /** * Limits this string to [maxLength] and adds [truncate] at the end if the string was shortened- */ -fun String.limit(maxLength: Int, truncate: String = "...") = if (length > maxLength) { +fun String.limit(maxLength: Int, truncate: String = "..."): String = if (length > maxLength) { substring(0, maxLength - truncate.length) + truncate } else { this } -fun List.format(transform: (T) -> CharSequence = { it.toString() }) = +fun List.format(transform: (T) -> CharSequence = { it.toString() }): String = joinToString(prefix = "`", separator = "`, `", postfix = "`", transform = transform) fun String.asNullable(): String? = ifBlank { null } diff --git a/src/main/kotlin/de/nycode/bankobot/utils/paginator/AbstractPaginator.kt b/src/main/kotlin/de/nycode/bankobot/utils/paginator/AbstractPaginator.kt new file mode 100644 index 0000000..36c1788 --- /dev/null +++ b/src/main/kotlin/de/nycode/bankobot/utils/paginator/AbstractPaginator.kt @@ -0,0 +1,202 @@ +/* + * This file is part of the BankoBot Project. + * Copyright (C) 2021 BankoBot Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * Also add information on how to contact you by electronic and paper mail. + * + * If your software can interact with users remotely through a computer + * network, you should also make sure that it provides a way for users to + * get its source. For example, if your program is a web application, its + * interface could display a "Source" link that leads users to an archive + * of the code. There are many ways you could offer source, and different + * solutions will be better for different programs; see section 13 for the + * specific requirements. + * + * You should also get your employer (if you work as a programmer) or school, + * if any, to sign a "copyright disclaimer" for the program, if necessary. + * For more information on this, and how to apply and follow the GNU AGPL, see + * . + * + */ + +package de.nycode.bankobot.utils.paginator + +import de.nycode.bankobot.BankoBot +import dev.kord.common.annotation.KordPreview +import dev.kord.rest.builder.message.EmbedBuilder +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.math.min +import kotlin.time.ExperimentalTime + +/** + * Context for paginator creation. + * + * @see paginate + */ +interface PaginatorContext { + + /** + * The [PaginatorContext]. + */ + val options: PaginatorOptions + + /** + * The method that renders the paginator embed. + */ + fun renderEmbed(rows: List, currentPage: Int): EmbedBuilder +} + +/** + * Creates a paginator for all elements in this [ItemProvider]. + * + * See List.paginate method above for default implementation + * + * @param title Shortcut to [PaginatorOptions.title] + * @param createPaginator lambda called on [PaginatorContext] which creates a paginator of the desired type + * @param sendOneMessage lambda called on [PaginatorContext] providing single [EmbedBuilder] + * to send un-paginated single page + * @see PaginatorOptions + * + * @see LazyItemProvider + * @see paginate + */ +@OptIn(KordPreview::class, ExperimentalTime::class) +internal suspend fun ItemProvider.paginate( + title: String? = null, + builder: PaginatorOptions.() -> Unit = {}, + createPaginator: suspend PaginatorContext.() -> Unit, + sendOneMessage: suspend PaginatorContext.(EmbedBuilder) -> Unit +) { + require(!isEmpty) { "Items must not be empty" } + val options = PaginatorOptions(this).apply { + if (title != null) { + this.title = title + } + }.apply(builder).ensureReady() + + fun renderEmbed(rows: List, currentPage: Int): EmbedBuilder { + return EmbedBuilder().apply { + color = options.color + this.title = options.title + val rowBuilder = StringBuilder() + rows.indices.forEach { + rowBuilder.append('`').append(it + (options.itemsPerPage * (currentPage - 1)) + 1) + .append("`. ") + .appendLine(rows[it]) + } + description = rowBuilder.toString() + footer { + text = "Seite $currentPage/${options.pages} (${length} Einträge)" + } + } + } + + val context = object : PaginatorContext { + override val options: PaginatorOptions + get() = options + + override fun renderEmbed(rows: List, currentPage: Int): EmbedBuilder = renderEmbed(rows, currentPage) + } + + if (options.pages > 1) { + createPaginator(context) + } else { + context.sendOneMessage(renderEmbed(this.subList(0, length), 1)) + } +} + +/** + * Abstract implementation of a paginator. + * + * @param renderEmbed a function which renders the embed for the given elements on the specified page + * @property options the [PaginatorOptions] + */ +@OptIn(KordPreview::class, ExperimentalTime::class) +abstract class AbstractPaginator constructor( + private val renderEmbed: (elements: List, page: Int) -> EmbedBuilder, + protected val options: PaginatorOptions, +) { + protected var currentPage = -1 + + private var canceller = timeout() + + private fun timeout(): Job { + return BankoBot.launch { + delay(options.timeout.inWholeMilliseconds) + close() + } + } + + private fun rescheduleTimeout() { + canceller.cancel() + canceller = timeout() + } + + /** + * Method supposed to be called on interaction with the paginator (reaction/button). + */ + protected suspend fun onInteraction(emoji: String) { + val nextPage = when (emoji) { + BULK_LEFT -> 1 + LEFT -> currentPage - 1 + RIGHT -> currentPage + 1 + BULK_RIGHT -> options.pages + else -> -1 + } + + if (nextPage == -1) { + close() + return + } + + rescheduleTimeout() + + if (nextPage !in 1..options.pages) return + + paginate(nextPage) + } + + /** + * Paginates to [page]. + */ + suspend fun paginate(page: Int) { + currentPage = page + val start: Int = (page - 1) * options.itemsPerPage + val end = min(options.items.length, page * options.itemsPerPage) + val rows = options.items.subList(start, end) + updateMessage(renderEmbed(rows, currentPage)) + } + + /** + * Call this close method to close override [close] to close own resources, + */ + protected suspend fun internalClose() { + if (canceller.isActive) canceller.cancel() + close() + } + + /** + * Add own hooks to [internalClose]. + */ + protected abstract suspend fun close() + + /** + * Updates the paginated message to [embedBuilder]. + */ + protected abstract suspend fun updateMessage(embedBuilder: EmbedBuilder) +} diff --git a/src/main/kotlin/de/nycode/bankobot/utils/paginator/InteractionsPaginator.kt b/src/main/kotlin/de/nycode/bankobot/utils/paginator/InteractionsPaginator.kt new file mode 100644 index 0000000..4326c77 --- /dev/null +++ b/src/main/kotlin/de/nycode/bankobot/utils/paginator/InteractionsPaginator.kt @@ -0,0 +1,119 @@ +package de.nycode.bankobot.utils.paginator + +import dev.kord.common.annotation.KordPreview +import dev.kord.common.entity.ButtonStyle +import dev.kord.common.entity.DiscordPartialEmoji +import dev.kord.common.entity.Snowflake +import dev.kord.core.behavior.interaction.PublicInteractionResponseBehavior +import dev.kord.core.behavior.interaction.edit +import dev.kord.core.entity.interaction.ComponentInteraction +import dev.kord.core.event.interaction.InteractionCreateEvent +import dev.kord.core.event.message.MessageDeleteEvent +import dev.kord.core.on +import dev.kord.rest.builder.component.ActionRowBuilder +import dev.kord.rest.builder.interaction.actionRow +import dev.kord.rest.builder.message.EmbedBuilder +import kotlinx.coroutines.Job +import kotlin.time.ExperimentalTime + +/** + * Creates a paginator for all items in this list. + * + * @see ItemProvider.paginate + */ +@OptIn(KordPreview::class) +suspend fun List.paginate( + interactionId: Snowflake, + response: PublicInteractionResponseBehavior, + title: String? = null, + builder: PaginatorOptions.() -> Unit = {}, +): Unit = DelegatedItemProvider(this).paginate(interactionId, response, title, builder) + +/** + * Creates a button based paginator for all elements in this [ItemProvider] in [response] + * + * See List.paginate method above for default implementation + * + * @param title Shortcut to [PaginatorOptions.title] + * @param interactionId the id of the interaction + * @see PaginatorOptions + * + * @see LazyItemProvider + */ +@OptIn(KordPreview::class, ExperimentalTime::class) +suspend fun ItemProvider.paginate( + interactionId: Snowflake, + response: PublicInteractionResponseBehavior, + title: String? = null, + builder: PaginatorOptions.() -> Unit = {}, +) = paginate(title, builder, { + InteractionsPaginator(interactionId, response, ::renderEmbed, options).paginate(options.firstPage) +}, { response.edit { embeds = mutableListOf(it) } }) + +@OptIn(KordPreview::class, ExperimentalTime::class) +private class InteractionsPaginator constructor( + private val interactionId: Snowflake, + private val message: PublicInteractionResponseBehavior, + renderEmbed: (List, Int) -> EmbedBuilder, + options: PaginatorOptions, +) : AbstractPaginator(renderEmbed, options) { + @OptIn(KordPreview::class) + private val listener: Job = message.kord + .on(consumer = ::handleEvent) + private val onDeleteListener = message.kord.on { + if (messageId == message?.id) { + internalClose() + } + } + + override suspend fun updateMessage(embedBuilder: EmbedBuilder) { + message.edit { + embeds = mutableListOf(embedBuilder) + actionRow { + prepareButtons() + } + + actionRow { + button(STOP, "cancel") + } + } + } + + override suspend fun close() { + message.edit { + components = mutableListOf() + } + listener.cancel() + onDeleteListener.cancel() + } + + private suspend fun handleEvent(event: InteractionCreateEvent) { + val componentInteraction = event.interaction as? ComponentInteraction ?: return + if (componentInteraction.message?.interaction?.id != interactionId) return + val component = componentInteraction.component ?: return + val emoji = component.data.emoji.value?.name ?: return + componentInteraction.acknowledgePublicDeferredMessageUpdate() + onInteraction(emoji) + } + + private fun ActionRowBuilder.button(emoji: String, name: String) = interactionButton(ButtonStyle.Primary, name) { + this.emoji = DiscordPartialEmoji(name = emoji) + } + + private fun ActionRowBuilder.prepareButtons() { + if (currentPage > 1) { // can go back + if (currentPage > 2) { // can go to beginning + button(BULK_LEFT, "bulkleft") + } + + button(LEFT, "left") + } + + if (currentPage < options.pages) { // can go forward + button(RIGHT, "right") + if (currentPage < options.pages - 1) { // can go to end + button(BULK_RIGHT, "bulkright") + } + } + } +} diff --git a/src/main/kotlin/de/nycode/bankobot/utils/paginator/MessagePaginator.kt b/src/main/kotlin/de/nycode/bankobot/utils/paginator/MessagePaginator.kt new file mode 100644 index 0000000..0360351 --- /dev/null +++ b/src/main/kotlin/de/nycode/bankobot/utils/paginator/MessagePaginator.kt @@ -0,0 +1,106 @@ +package de.nycode.bankobot.utils.paginator + +import de.nycode.bankobot.BankoBot +import de.nycode.bankobot.utils.Embeds.createEmbed +import de.nycode.bankobot.utils.Embeds.editEmbed +import de.nycode.bankobot.utils.doExpensiveTask +import dev.kord.common.annotation.KordPreview +import dev.kord.core.behavior.channel.MessageChannelBehavior +import dev.kord.core.entity.ReactionEmoji +import dev.kord.core.event.Event +import dev.kord.core.event.message.MessageDeleteEvent +import dev.kord.core.event.message.ReactionAddEvent +import dev.kord.core.event.message.ReactionRemoveEvent +import dev.kord.core.live.LiveMessage +import dev.kord.core.live.live +import dev.kord.rest.builder.message.EmbedBuilder +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlin.time.ExperimentalTime + +private val ALL = listOf(BULK_LEFT, LEFT, STOP, RIGHT, BULK_RIGHT) + +/** + * Creates a reaction based paginator for all elements in this [ItemProvider] in [channel] + * + * See List.paginate method above for default implementation + * + * @param title Shortcut to [PaginatorOptions.title] + * @see PaginatorOptions + * + * @see LazyItemProvider + */ +@OptIn(KordPreview::class, ExperimentalTime::class) +suspend fun ItemProvider.paginate( + channel: MessageChannelBehavior, + title: String? = null, + builder: PaginatorOptions.() -> Unit = {}, +) = paginate(title, builder, { + channel.doExpensiveTask(options.loadingTitle, options.loadingDescription) { + ALL.forEach { + addReaction(ReactionEmoji.Unicode(it)) + } + + MessagePaginator(asMessage().live(), ::renderEmbed, options).paginate(options.firstPage) + } +}, { channel.createEmbed(it) }) + +/** + * Creates a paginator for all items in this list. + * + * @see ItemProvider.paginate + */ +suspend fun List.paginate( + channel: MessageChannelBehavior, + title: String? = null, + builder: PaginatorOptions.() -> Unit = {}, +): Unit = DelegatedItemProvider(this).paginate(channel, title, builder) + +@OptIn(KordPreview::class, ExperimentalTime::class) +private class MessagePaginator constructor( + private val message: LiveMessage, + renderEmbed: (List, Int) -> EmbedBuilder, + options: PaginatorOptions, +) : AbstractPaginator(renderEmbed, options) { + @OptIn(KordPreview::class) + private val listener: Job = message + .events + .onEach(::handleEvent) + .launchIn(BankoBot) + + override suspend fun updateMessage(embedBuilder: EmbedBuilder) { + message.message.editEmbed(embedBuilder) + } + + override suspend fun close() { + message.message.deleteAllReactions() + listener.cancel() + } + + private suspend fun onReactionAdd(event: ReactionAddEvent) { + if (event.user.id != event.kord.selfId) { + event.message.deleteReaction(event.userId, event.emoji) + } else return // Don't react to the bots reactions + val emote = event.emoji.name + if (emote !in ALL) return + + onInteraction(event.emoji.name) + } + + private suspend fun onReactionRemove(event: ReactionRemoveEvent) { + if (event.emoji.name in ALL) { + message.message.addReaction(event.emoji) + } + } + + private suspend fun onDelete() = internalClose() + + private suspend fun handleEvent(event: Event) { + when (event) { + is ReactionAddEvent -> onReactionAdd(event) + is ReactionRemoveEvent -> onReactionRemove(event) + is MessageDeleteEvent -> onDelete() + } + } +} diff --git a/src/main/kotlin/de/nycode/bankobot/utils/Paginator.kt b/src/main/kotlin/de/nycode/bankobot/utils/paginator/Paginator.kt similarity index 50% rename from src/main/kotlin/de/nycode/bankobot/utils/Paginator.kt rename to src/main/kotlin/de/nycode/bankobot/utils/paginator/Paginator.kt index c4978e8..c640eb8 100644 --- a/src/main/kotlin/de/nycode/bankobot/utils/Paginator.kt +++ b/src/main/kotlin/de/nycode/bankobot/utils/paginator/Paginator.kt @@ -32,35 +32,26 @@ * */ -package de.nycode.bankobot.utils +package de.nycode.bankobot.utils.paginator -import de.nycode.bankobot.BankoBot -import de.nycode.bankobot.utils.Embeds.createEmbed -import de.nycode.bankobot.utils.Embeds.editEmbed +import de.nycode.bankobot.command.Context +import de.nycode.bankobot.command.DelegatedKordCommandContext +import de.nycode.bankobot.command.slashcommands.SlashCommandContext +import de.nycode.bankobot.utils.Colors import dev.kord.common.Color import dev.kord.common.annotation.KordPreview -import dev.kord.core.behavior.channel.MessageChannelBehavior -import dev.kord.core.entity.ReactionEmoji -import dev.kord.core.event.Event -import dev.kord.core.event.message.MessageDeleteEvent -import dev.kord.core.event.message.ReactionAddEvent -import dev.kord.core.event.message.ReactionRemoveEvent -import dev.kord.core.live.LiveMessage -import dev.kord.core.live.live -import dev.kord.rest.builder.message.EmbedBuilder import dev.kord.x.emoji.Emojis -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import org.litote.kmongo.coroutine.CoroutineFindPublisher import kotlin.math.ceil -import kotlin.math.min import kotlin.properties.Delegates import kotlin.time.Duration import kotlin.time.ExperimentalTime -import kotlin.time.seconds + +internal val BULK_LEFT: String = Emojis.rewind.unicode +internal val LEFT: String = Emojis.arrowLeft.unicode +internal val STOP: String = Emojis.stopButton.unicode +internal val RIGHT: String = Emojis.arrowRight.unicode +internal val BULK_RIGHT: String = Emojis.fastForward.unicode /** * Interface to provide paginateable items @@ -84,7 +75,7 @@ interface ItemProvider { } @Suppress("FunctionName") -fun LazyItemProvider(length: Int, subList: suspend (Int, Int) -> List) = +fun LazyItemProvider(length: Int, subList: suspend (Int, Int) -> List): ItemProvider = object : ItemProvider { override val length: Int = length @@ -132,18 +123,22 @@ class PaginatorOptions(val items: ItemProvider) { } /** - * Creates a paginator for all items in this list. + * Implementation of [ItemProvider] which delegates to [list]. * - * @see ItemProvider.paginate + * @see List */ -suspend fun List.paginate( - channel: MessageChannelBehavior, - title: String? = null, - builder: PaginatorOptions.() -> Unit = {}, -) = DelegatedItemProvider(this).paginate(channel, title, builder) +class DelegatedItemProvider(private val list: List) : ItemProvider { + override val length: Int + get() = list.size + + override suspend fun subList(startIndex: Int, endIndex: Int): List = + list.subList(startIndex, endIndex) +} /** - * Creates a reaction based paginator for all elements in this [ItemProvider] in [channel] + * Creates a context-aware paginator for all elements in this [ItemProvider] using [context]. + * + * **Note:** this will use buttons for interactions and reactions for message commands * * See List.paginate method above for default implementation * @@ -154,147 +149,30 @@ suspend fun List.paginate( */ @OptIn(KordPreview::class, ExperimentalTime::class) suspend fun ItemProvider.paginate( - channel: MessageChannelBehavior, + context: Context, title: String? = null, builder: PaginatorOptions.() -> Unit = {}, -) { - require(!isEmpty) { "Items must not be empty" } - val options = PaginatorOptions(this).apply { - if (title != null) { - this.title = title - } - }.apply(builder).ensureReady() - - fun renderEmbed(rows: List, currentPage: Int): EmbedBuilder { - return EmbedBuilder().apply { - color = options.color - this.title = options.title - val rowBuilder = StringBuilder() - rows.indices.forEach { - rowBuilder.append('`').append(it + (options.itemsPerPage * (currentPage - 1)) + 1) - .append("`. ") - .appendLine(rows[it]) - } - description = rowBuilder.toString() - footer { - text = "Seite $currentPage/${options.pages} (${length} Einträge)" - } - } - } - - if (options.pages > 1) { - channel.doExpensiveTask(options.loadingTitle, options.loadingDescription) { - ALL.forEach { - addReaction(ReactionEmoji.Unicode(it)) - } - - Paginator(asMessage().live(), ::renderEmbed, options).paginate(options.firstPage) - } - } else { - channel.createEmbed(renderEmbed(this.subList(0, length), 1)) - } -} - -private val BULK_LEFT: String = Emojis.rewind.unicode -private val LEFT: String = Emojis.arrowLeft.unicode -private val STOP: String = Emojis.stopButton.unicode -private val RIGHT: String = Emojis.arrowRight.unicode -private val BULK_RIGHT: String = Emojis.fastForward.unicode - -private val ALL = listOf(BULK_LEFT, LEFT, STOP, RIGHT, BULK_RIGHT) - -private class DelegatedItemProvider(private val list: List) : ItemProvider { - override val length: Int - get() = list.size - - override suspend fun subList(startIndex: Int, endIndex: Int): List = - list.subList(startIndex, endIndex) -} - -@OptIn(KordPreview::class, ExperimentalTime::class) -private class Paginator constructor( - private val message: LiveMessage, - private val renderEmbed: (List, Int) -> EmbedBuilder, - private val options: PaginatorOptions, -) { - private var currentPage = -1 - - @OptIn(KordPreview::class) - private val listener: Job = message - .events - .onEach(::handleEvent) - .launchIn(BankoBot) - - private var canceller = timeout() - - private fun timeout(): Job { - return BankoBot.launch { - delay(options.timeout.inWholeMilliseconds) - close() - } - } - - private fun rescheduleTimeout() { - canceller.cancel() - canceller = timeout() - } - - private suspend fun onReactionAdd(event: ReactionAddEvent) { - if (event.user.id != event.kord.selfId) { - event.message.deleteReaction(event.userId, event.emoji) - } else return // Don't react to the bots reactions - val emote = event.emoji.name - if (emote !in ALL) return - - val nextPage = when (emote) { - BULK_LEFT -> 1 - LEFT -> currentPage - 1 - RIGHT -> currentPage + 1 - BULK_RIGHT -> options.pages - else -> -1 - } - - if (nextPage == -1) { - close() - return - } - - rescheduleTimeout() - - if (nextPage !in 1..options.pages) return - - paginate(nextPage) - } - - private suspend fun onReactionRemove(event: ReactionRemoveEvent) { - if (event.emoji.name in ALL) { - message.message.addReaction(event.emoji) - } - } - - private suspend fun onDelete() = close() - - suspend fun paginate(page: Int) { - currentPage = page - val start: Int = (page - 1) * options.itemsPerPage - val end = min(options.items.length, page * options.itemsPerPage) - val rows = options.items.subList(start, end) - message.message.editEmbed(renderEmbed(rows, currentPage)) - } - - private suspend fun close() { - message.message.deleteAllReactions() - if (canceller.isActive) canceller.cancel() - listener.cancel() +) = + when (context) { + is DelegatedKordCommandContext -> paginate(context.channel, title, builder) // message context + is SlashCommandContext -> paginate(context.interactionId, context.ack, title, builder) // public slash command + else -> error("ephemerals are not supported due to unpredictable delete behavior") } - private suspend fun handleEvent(event: Event) { - when (event) { - is ReactionAddEvent -> onReactionAdd(event) - is ReactionRemoveEvent -> onReactionRemove(event) - is MessageDeleteEvent -> onDelete() - } - } +/** + * Creates a context-aware paginator for all items in this list. + * + * @see ItemProvider.paginate + */ +@OptIn(KordPreview::class) +suspend fun List.paginate( + context: Context, + title: String? = null, + builder: PaginatorOptions.() -> Unit = {}, +): Unit = when (context) { + is DelegatedKordCommandContext -> paginate(context.channel, title, builder) // message context + is SlashCommandContext -> paginate(context.interactionId, context.ack, title, builder) // public slash command + else -> error("ephemerals are not supported due to unpredictable delete behavior") } /** @@ -307,7 +185,7 @@ suspend fun CoroutineFindPublisher.paginate( start: Int, pageSize: Int = 8, result: (T) -> String -) = +): List = skip(start) .limit(pageSize) .toList() diff --git a/src/main/kotlin/de/nycode/bankobot/variables/parsers/calc/CalcExpression.kt b/src/main/kotlin/de/nycode/bankobot/variables/parsers/calc/CalcExpression.kt index b5292ed..394e639 100644 --- a/src/main/kotlin/de/nycode/bankobot/variables/parsers/calc/CalcExpression.kt +++ b/src/main/kotlin/de/nycode/bankobot/variables/parsers/calc/CalcExpression.kt @@ -42,21 +42,20 @@ import io.ktor.client.features.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.util.* +import kotlin.time.Duration import kotlin.time.ExperimentalTime -import kotlin.time.seconds class CalcExpression(val expression: String) : Expression { private var result: CalcExpressionResult? = null companion object { - @OptIn(KtorExperimentalAPI::class, ExperimentalTime::class) + @OptIn(ExperimentalTime::class) private val httpClient = HttpClient(CIO) { expectSuccess = false install(HttpTimeout) { - requestTimeoutMillis = 10.seconds.inWholeMilliseconds - connectTimeoutMillis = 10.seconds.inWholeMilliseconds + requestTimeoutMillis = Duration.seconds(10).inWholeMilliseconds + connectTimeoutMillis = Duration.seconds(10).inWholeMilliseconds } } }