From 040f3f3cdf26cf73514a75ab90d5d9d0081d765c Mon Sep 17 00:00:00 2001 From: schlaubi Date: Sat, 5 Jun 2021 21:58:17 +0200 Subject: [PATCH 01/12] Update Gradle version --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f371643..29e4134 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From cdabd7f60ecaf1737aad9ae2e216d9defb86b65f Mon Sep 17 00:00:00 2001 From: schlaubi Date: Sat, 5 Jun 2021 22:09:32 +0200 Subject: [PATCH 02/12] Add Sentry and log level config --- build.gradle.kts | 2 + .../kotlin/dev/schlaubi/votebot/Launcher.kt | 48 +++++++++++++++++++ .../kotlin/dev/schlaubi/votebot/VoteBot.kt | 23 +++++++++ .../dev/schlaubi/votebot/config/Config.kt | 35 ++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 src/main/kotlin/dev/schlaubi/votebot/Launcher.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/VoteBot.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/config/Config.kt diff --git a/build.gradle.kts b/build.gradle.kts index f767d28..8cb57e6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,4 +21,6 @@ dependencies { implementation("dev.schlaubi", "envconf", "1.0") implementation("ch.qos.logback", "logback-classic", "1.3.0-alpha5") + implementation("io.sentry", "sentry", "4.3.0") + implementation("io.sentry", "sentry-logback", "4.3.0") } diff --git a/src/main/kotlin/dev/schlaubi/votebot/Launcher.kt b/src/main/kotlin/dev/schlaubi/votebot/Launcher.kt new file mode 100644 index 0000000..55e0f26 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/Launcher.kt @@ -0,0 +1,48 @@ +/* + * VoteBot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot + +import ch.qos.logback.classic.Logger +import dev.schlaubi.votebot.config.Config +import dev.schlaubi.votebot.config.Environment +import io.sentry.Sentry +import io.sentry.SentryOptions +import org.slf4j.LoggerFactory + +suspend fun main() { + initializeLogging() + initializeSentry() +} + +private fun initializeLogging() { + val rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger + rootLogger.level = Config.LOG_LEVEL +} + +private fun initializeSentry() { + val configure: (SentryOptions) -> Unit = + if (Config.ENVIRONMENT != Environment.DEVELOPMENT) { + { it.dsn = Config.SENTRY_TOKEN; } + } else { + { it.dsn = "" } + } + + Sentry.init(configure) +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/VoteBot.kt b/src/main/kotlin/dev/schlaubi/votebot/VoteBot.kt new file mode 100644 index 0000000..d67c58e --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/VoteBot.kt @@ -0,0 +1,23 @@ +/* + * VoteBot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot + +class VoteBot { +} \ No newline at end of file diff --git a/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt b/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt new file mode 100644 index 0000000..df67a2c --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt @@ -0,0 +1,35 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.config + +import ch.qos.logback.classic.Level +import dev.schlaubi.envconf.getEnv + +object Config { + val ENVIRONMENT by getEnv("", Environment.PRODUCTION, Environment::valueOf) + val LOG_LEVEL: Level by getEnv(default = Level.INFO) { Level.toLevel(it) } + + val SENTRY_TOKEN by getEnv().optional() +} + +enum class Environment(val useSentry: Boolean = true) { + PRODUCTION, + DEVELOPMENT(false) +} From e84fc30406db688b77a59eedd7f4fd405c45b6a7 Mon Sep 17 00:00:00 2001 From: schlaubi Date: Sun, 6 Jun 2021 20:34:49 +0200 Subject: [PATCH 03/12] Add initial commands --- .idea/uiDesigner.xml | 124 ++++++++++++++++++ build.gradle.kts | 15 ++- .../kotlin/dev/schlaubi/votebot/Launcher.kt | 13 +- .../votebot/command/CommandErrorHandler.kt | 33 +++++ .../votebot/command/DescriptiveCommand.kt | 31 +++++ .../votebot/command/ExecutableCommand.kt | 39 ++++++ .../votebot/command/RegistrableCommand.kt | 42 ++++++ .../schlaubi/votebot/command/RootCommand.kt | 45 +++++++ .../schlaubi/votebot/command/SingleCommand.kt | 29 ++++ .../schlaubi/votebot/command/SubCommand.kt | 36 +++++ .../votebot/command/context/Arguments.kt | 110 ++++++++++++++++ .../votebot/command/context/Context.kt | 88 +++++++++++++ .../response/EphemeralResponseStrategy.kt | 51 +++++++ .../response/FollowUpResponseStrategy.kt | 62 +++++++++ .../context/response/NonEditableResponse.kt | 27 ++++ .../response/PublicResponseStrategy.kt | 66 ++++++++++ .../command/context/response/Responder.kt | 118 +++++++++++++++++ .../command/internal/CommandExecutor.kt | 119 +++++++++++++++++ .../votebot/command/internal/ContextImpl.kt | 40 ++++++ .../command/internal/DebugErrorHandler.kt | 37 ++++++ .../commands/ClaimPermissionsCommand.kt | 54 ++++++++ .../schlaubi/votebot/commands/InfoCommand.kt | 34 +++++ .../votebot/commands/PermissionCommand.kt | 121 +++++++++++++++++ .../dev/schlaubi/votebot/config/Config.kt | 60 ++++++++- .../dev/schlaubi/votebot/core/VoteBot.kt | 40 ++++++ .../dev/schlaubi/votebot/core/VoteBotImpl.kt | 58 ++++++++ .../votebot/util/CommandRegistrationUtil.kt | 60 +++++++++ .../{VoteBot.kt => util/EmbedUtils.kt} | 14 +- .../dev/schlaubi/votebot/util/OptionalUtil.kt | 27 ++++ .../schlaubi/votebot/util/PermissionUtil.kt | 65 +++++++++ src/main/resources/logback.xml | 3 - 31 files changed, 1648 insertions(+), 13 deletions(-) create mode 100644 .idea/uiDesigner.xml create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/CommandErrorHandler.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/DescriptiveCommand.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/ExecutableCommand.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/RegistrableCommand.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/RootCommand.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/SingleCommand.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/SubCommand.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/context/Arguments.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/context/Context.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/context/response/EphemeralResponseStrategy.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/context/response/FollowUpResponseStrategy.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/context/response/NonEditableResponse.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/context/response/PublicResponseStrategy.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/context/response/Responder.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/internal/CommandExecutor.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/internal/ContextImpl.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/internal/DebugErrorHandler.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/core/VoteBot.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/util/CommandRegistrationUtil.kt rename src/main/kotlin/dev/schlaubi/votebot/{VoteBot.kt => util/EmbedUtils.kt} (71%) create mode 100644 src/main/kotlin/dev/schlaubi/votebot/util/OptionalUtil.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/util/PermissionUtil.kt diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..e96534f --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8cb57e6..682713c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,13 +14,26 @@ repositories { } dependencies { - implementation("io.github.microutils", "kotlin-logging", "2.0.8") implementation("dev.kord", "kord-core", "0.7.x-SNAPSHOT") implementation("dev.schlaubi", "envconf", "1.0") + implementation("io.github.microutils", "kotlin-logging", "2.0.8") implementation("ch.qos.logback", "logback-classic", "1.3.0-alpha5") implementation("io.sentry", "sentry", "4.3.0") implementation("io.sentry", "sentry-logback", "4.3.0") } + +tasks { + withType { + kotlinOptions { + freeCompilerArgs = + listOf( + "-Xopt-in=dev.kord.common.annotation.KordPreview", + "-Xopt-in=kotlin.RequiresOptIn", + "-Xopt-in=kotlin.ExperimentalStdlibApi" + ) + } + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/Launcher.kt b/src/main/kotlin/dev/schlaubi/votebot/Launcher.kt index 55e0f26..1f00ba1 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/Launcher.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/Launcher.kt @@ -21,24 +21,33 @@ package dev.schlaubi.votebot import ch.qos.logback.classic.Logger import dev.schlaubi.votebot.config.Config -import dev.schlaubi.votebot.config.Environment import io.sentry.Sentry import io.sentry.SentryOptions +import mu.KotlinLogging import org.slf4j.LoggerFactory +import dev.schlaubi.votebot.core.VoteBotImpl as VoteBot + +private val LOG = KotlinLogging.logger { } suspend fun main() { initializeLogging() initializeSentry() + + VoteBot().start() } private fun initializeLogging() { val rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger rootLogger.level = Config.LOG_LEVEL + + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + LOG.error(throwable) { "Got unhandled error on $thread" } + } } private fun initializeSentry() { val configure: (SentryOptions) -> Unit = - if (Config.ENVIRONMENT != Environment.DEVELOPMENT) { + if (Config.ENVIRONMENT.useSentry) { { it.dsn = Config.SENTRY_TOKEN; } } else { { it.dsn = "" } diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/CommandErrorHandler.kt b/src/main/kotlin/dev/schlaubi/votebot/command/CommandErrorHandler.kt new file mode 100644 index 0000000..12c5858 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/CommandErrorHandler.kt @@ -0,0 +1,33 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command + +import dev.schlaubi.votebot.command.context.Context +import kotlin.coroutines.CoroutineContext + +/** + * Functional interface to properly handle Errors during command execution. + */ +fun interface CommandErrorHandler { + /** + * Handels a [throwable] that was thrown in [context] and [coroutineContext]. + */ + suspend fun handleCommandError(context: Context, coroutineContext: CoroutineContext, throwable: Throwable) +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/DescriptiveCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/command/DescriptiveCommand.kt new file mode 100644 index 0000000..b1aa8e0 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/DescriptiveCommand.kt @@ -0,0 +1,31 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command + +/** + * A command which has registration data. + * + * @property name the name of the command + * @property description the description of the command + */ +sealed interface DescriptiveCommand { + val name: String + val description: String +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/ExecutableCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/command/ExecutableCommand.kt new file mode 100644 index 0000000..837235a --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/ExecutableCommand.kt @@ -0,0 +1,39 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command + +import dev.schlaubi.votebot.command.context.Context + +/** + * A command which has execution logic. + */ +sealed interface ExecutableCommand { + + /** + * Whether [Context.respond] functions should use ephemeral responses or not. + */ + val useEphemeral: Boolean + get() = false + + /** + * Executes the command logic in [context], + */ + suspend fun execute(context: Context) +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/RegistrableCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/command/RegistrableCommand.kt new file mode 100644 index 0000000..3cc98db --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/RegistrableCommand.kt @@ -0,0 +1,42 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command + +import dev.kord.rest.builder.interaction.ApplicationCommandCreateBuilder + +/** + * A command which can be sent to Discord at root level. + * + * @see DescriptiveCommand + */ +sealed interface RegistrableCommand : DescriptiveCommand { + + /** + * The default permission of the command. + */ + val defaultPermission: Boolean + get() = true + + /** + * Optional extension of [ApplicationCommandCreateBuilder] to add needed arguments of the command. + */ + fun ApplicationCommandCreateBuilder.addArguments() { + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/RootCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/command/RootCommand.kt new file mode 100644 index 0000000..15d4f41 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/RootCommand.kt @@ -0,0 +1,45 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command + +import dev.kord.rest.builder.interaction.ApplicationCommandCreateBuilder +import dev.schlaubi.votebot.util.buildCommands + +/** + * Abstract command that has sub commands. + * + * @property subCommands the subcommand of this command + * @see buildCommands + * @see RegistrableCommand + * @see DescriptiveCommand + */ +abstract class RootCommand( + val subCommands: Map +) : RegistrableCommand, DescriptiveCommand { + final override fun ApplicationCommandCreateBuilder.addArguments() { + subCommands.forEach { (_, subCommand) -> + subCommand(subCommand.name, subCommand.description) { + with(subCommand) { + addArguments() + } + } + } + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/SingleCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/command/SingleCommand.kt new file mode 100644 index 0000000..f8074c8 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/SingleCommand.kt @@ -0,0 +1,29 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command + +/** + * A Single command without any subcommands (has execution logic). + * + * @see ExecutableCommand + * @see DescriptiveCommand + * @see RegistrableCommand + */ +abstract class SingleCommand : ExecutableCommand, DescriptiveCommand, RegistrableCommand diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/SubCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/command/SubCommand.kt new file mode 100644 index 0000000..f88a636 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/SubCommand.kt @@ -0,0 +1,36 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command + +import dev.kord.rest.builder.interaction.SubCommandBuilder + +/** + * A sub command of a [RootCommand]. + * + * @see DescriptiveCommand + * @see ExecutableCommand + */ +abstract class SubCommand : DescriptiveCommand, ExecutableCommand { + + /** + * Optional override of [SubCommandBuilder] to add needed arguments. + */ + open fun SubCommandBuilder.addArguments() = Unit +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/Arguments.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/Arguments.kt new file mode 100644 index 0000000..0ca5a5e --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/Arguments.kt @@ -0,0 +1,110 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.context + +import dev.kord.core.entity.Member +import dev.kord.core.entity.Role +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.GuildChannel +import dev.kord.core.entity.interaction.OptionValue + +/** + * Container for command options. + */ +interface Arguments { + /** + * Returns an nullable [String] for [name]. + */ + fun optionalString(name: String): String? = optionalArgument(name)?.cast()?.value + + /** + * Returns a non-nullable [String] for [name]. + */ + fun string(name: String): String = argument(name).cast().value + + /** + * Returns an nullable [Int] for [name]. + */ + fun optionalInt(name: String): Int? = optionalArgument(name)?.cast()?.value + + /** + * Returns a non-nullable [Int] for [name]. + */ + fun int(name: String): Int = argument(name).cast().value + + /** + * Returns an nullable [Boolean] for [name]. + */ + fun optionalBoolean(name: String): Boolean? = optionalArgument(name)?.cast()?.value + + /** + * Returns a non-nullable [Boolean] for [name]. + */ + fun boolean(name: String): Boolean = argument(name).cast().value + + /** + * Returns an nullable [User] for [name]. + */ + fun optionalUser(name: String): User? = optionalArgument(name)?.cast()?.value + + /** + * Returns a non-nullable [User] for [name]. + */ + fun user(name: String): User = argument(name).cast().value + + /** + * Returns an nullable [Member] for [name]. + */ + fun optionalMember(name: String): Member? = optionalArgument(name)?.cast()?.value + + /** + * Returns a non-nullable [Member] for [name]. + */ + fun member(name: String): Member = argument(name).cast().value + + /** + * Returns an nullable [Role] for [name]. + */ + fun optionalRole(name: String): Role? = optionalArgument(name)?.cast()?.value + + /** + * Returns a non-nullable [Role] for [name]. + */ + fun role(name: String): Role = argument(name).cast().value + + /** + * Returns an nullable [GuildChannel] for [name]. + */ + fun optionalChannel(name: String): GuildChannel? = optionalArgument(name)?.cast()?.value + + /** + * Returns a non-nullable [GuildChannel] for [name]. + */ + fun channel(name: String): GuildChannel = argument(name).cast().value + + /** + * Finds an nullable [OptionValue] for name. + */ + fun optionalArgument(name: String): OptionValue<*>? + private fun argument(name: String): OptionValue<*> = + optionalArgument(name) ?: error("Could not find argument for name: $name") + @Suppress("UNCHECKED_CAST") + private fun OptionValue<*>.cast(): OptionValue = this as OptionValue +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/Context.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/Context.kt new file mode 100644 index 0000000..39ead56 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/Context.kt @@ -0,0 +1,88 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.context + +import dev.kord.common.annotation.KordExperimental +import dev.kord.common.annotation.KordUnsafe +import dev.kord.core.Kord +import dev.kord.core.behavior.GuildBehavior +import dev.kord.core.behavior.MemberBehavior +import dev.kord.core.behavior.UserBehavior +import dev.kord.core.entity.interaction.GuildInteraction +import dev.kord.core.entity.interaction.InteractionCommand +import dev.schlaubi.votebot.command.ExecutableCommand +import dev.schlaubi.votebot.command.context.response.Responder +import dev.schlaubi.votebot.core.VoteBot + +/** + * The context in which an command gets executed. + * + * @see Arguments + * @see Responder + */ +interface Context : Arguments, Responder { + /** + * The [VoteBot] instance which triggered this command + */ + val bot: VoteBot + + /** + * The [Kord] instance which triggered this command. + */ + val kord: Kord get() = bot.kord + + /** + * The [ExecutableCommand] which was executed (refers to `this` in command classes) + */ + val command: ExecutableCommand + + /** + * The [GuildInteraction] which triggered this command. + */ + val interaction: GuildInteraction + + /** + * The [InteractionCommand] which got executed. + */ + val slashCommand: InteractionCommand get() = interaction.command + + /** + * The user who executed this command. + */ + val executor: MemberBehavior get() = interaction.member + + /** + * The member of the bot on [guild]. + */ + @OptIn(KordUnsafe::class, KordExperimental::class) + val me: MemberBehavior get() = interaction.kord.unsafe.member(interaction.guildId, interaction.kord.selfId) + + /** + * The [GuildBehavior] on which the command was executed + */ + val guild: GuildBehavior get() = interaction.guild + + /** + * User equivalent of [me]. + */ + @OptIn(KordUnsafe::class, KordExperimental::class) + val selfUser: UserBehavior + get() = interaction.kord.unsafe.user(interaction.kord.selfId) +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/EphemeralResponseStrategy.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/EphemeralResponseStrategy.kt new file mode 100644 index 0000000..23194e1 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/EphemeralResponseStrategy.kt @@ -0,0 +1,51 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.context.response + +import dev.kord.common.annotation.KordUnsafe +import dev.kord.common.entity.optional.Optional +import dev.kord.core.behavior.interaction.EphemeralInteractionResponseBehavior +import dev.kord.core.behavior.interaction.followUp +import dev.kord.core.entity.interaction.PublicFollowupMessage +import dev.kord.rest.builder.interaction.PublicFollowupMessageCreateBuilder +import dev.kord.rest.builder.message.MessageCreateBuilder +import dev.kord.rest.json.request.InteractionResponseModifyRequest +import dev.schlaubi.votebot.util.optional + +internal class EphemeralResponseStrategy(private val ack: EphemeralInteractionResponseBehavior) : + FollowUpResponseStrategy(), Responder { + override suspend fun respond(builder: MessageCreateBuilder.() -> Unit): Responder.EditableResponse { + val message = MessageCreateBuilder().apply(builder) + val embeds = listOfNotNull(message.embed?.toRequest()) + val request = InteractionResponseModifyRequest( + message.content.optional(), + Optional.missingOnEmpty(embeds), + message.allowedMentions?.build().optional() + ) + + ack.kord.rest.interaction.modifyInteractionResponse(ack.applicationId, ack.token, request) + + return NonEditableResponse + } + + @OptIn(KordUnsafe::class) + override suspend fun internalFollowUp(builder: PublicFollowupMessageCreateBuilder.() -> Unit): PublicFollowupMessage = + ack.followUp(builder) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/FollowUpResponseStrategy.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/FollowUpResponseStrategy.kt new file mode 100644 index 0000000..5aec3aa --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/FollowUpResponseStrategy.kt @@ -0,0 +1,62 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.context.response + +import dev.kord.core.behavior.interaction.edit +import dev.kord.core.entity.interaction.PublicFollowupMessage +import dev.kord.rest.builder.interaction.PublicFollowupMessageCreateBuilder +import dev.kord.rest.builder.message.MessageCreateBuilder +import dev.kord.rest.builder.message.MessageModifyBuilder + +internal sealed class FollowUpResponseStrategy : Responder { + protected abstract suspend fun internalFollowUp(builder: PublicFollowupMessageCreateBuilder.() -> Unit): PublicFollowupMessage + + final override suspend fun followUp(builder: MessageCreateBuilder.() -> Unit): Responder.EditableResponse { + val message = MessageCreateBuilder().apply(builder) + val response = internalFollowUp { + content = message.content + message.embed?.let { + embeds = mutableListOf(it.toRequest()) + } + allowedMentions = message.allowedMentions?.build() + message.files.forEach { (name, inputStream) -> + addFile(name, inputStream) + } + } + + return PublicFollowUpEditableResponse(response) + } + + private class PublicFollowUpEditableResponse(private val response: PublicFollowupMessage) : + Responder.EditableResponse { + override suspend fun edit(builder: MessageModifyBuilder.() -> Unit): Responder.EditableResponse { + val message = MessageModifyBuilder().apply(builder) + response.edit { + content = message.content + message.embed?.let { + embeds = mutableListOf(it) + } + allowedMentions = message.allowedMentions + } + + return this + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/NonEditableResponse.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/NonEditableResponse.kt new file mode 100644 index 0000000..0f4617e --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/NonEditableResponse.kt @@ -0,0 +1,27 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.context.response + +import dev.kord.rest.builder.message.MessageModifyBuilder + +internal object NonEditableResponse : Responder.EditableResponse { + override suspend fun edit(builder: MessageModifyBuilder.() -> Unit): Responder.EditableResponse = + throw UnsupportedOperationException("This type of message does not support editing") +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/PublicResponseStrategy.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/PublicResponseStrategy.kt new file mode 100644 index 0000000..589fa7b --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/PublicResponseStrategy.kt @@ -0,0 +1,66 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.context.response + +import dev.kord.core.behavior.edit +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.Message +import dev.kord.core.entity.interaction.PublicFollowupMessage +import dev.kord.rest.builder.interaction.PublicFollowupMessageCreateBuilder +import dev.kord.rest.builder.message.MessageCreateBuilder +import dev.kord.rest.builder.message.MessageModifyBuilder + +internal class PublicResponseStrategy(private val ack: PublicInteractionResponseBehavior) : FollowUpResponseStrategy(), + Responder { + override suspend fun internalFollowUp(builder: PublicFollowupMessageCreateBuilder.() -> Unit): PublicFollowupMessage = + ack.followUp(builder) + + override suspend fun respond(builder: MessageCreateBuilder.() -> Unit): Responder.EditableResponse { + val message = MessageCreateBuilder().apply(builder) + val response = ack.edit { + content = message.content + message.embed?.let { + embeds = mutableListOf(it) + } + allowedMentions = message.allowedMentions + message.files.forEach { (name, inputStream) -> + addFile(name, inputStream) + } + } + + return MessageEditableResponse(response) + } + + + private class MessageEditableResponse(private val sentMessage: Message) : Responder.EditableResponse { + override suspend fun edit(builder: MessageModifyBuilder.() -> Unit): Responder.EditableResponse { + val message = MessageModifyBuilder().apply(builder) + sentMessage.edit { + content = message.content + embed = message.embed + allowedMentions = message.allowedMentions + } + + return this + } + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/Responder.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/Responder.kt new file mode 100644 index 0000000..c02f1f9 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/Responder.kt @@ -0,0 +1,118 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.context.response + +import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.rest.builder.message.MessageCreateBuilder +import dev.kord.rest.builder.message.MessageModifyBuilder +import dev.schlaubi.votebot.command.ExecutableCommand +/** + * Responding logic for a command execution. + * + * @see ExecutableCommand.useEphemeral + */ +interface Responder { + /** + * Responds to the command by editing the original ack. + */ + suspend fun respond(builder: MessageCreateBuilder.() -> Unit): EditableResponse + + /** + * Sends a follow-up in the command thread. + */ + suspend fun followUp(builder: MessageCreateBuilder.() -> Unit): EditableResponse + + /** + * Editing logic for a sent response. + */ + sealed interface EditableResponse { + /** + * Edits the response. + */ + suspend fun edit(builder: MessageModifyBuilder.() -> Unit): EditableResponse + } +} + +/** + * Responds to the command by editing the original ack. + */ +suspend inline fun Responder.respond(message: String): Responder.EditableResponse = respond { + content = message +} + +/** + * Responds to the command by editing the original ack. + */ +suspend inline fun Responder.respond(embed: EmbedBuilder): Responder.EditableResponse = respond { + this.embed = embed +} + +/** + * Responds to the command by editing the original ack. + */ +suspend inline fun Responder.respondEmbed(crossinline embed: EmbedBuilder.() -> Unit): Responder.EditableResponse = + respond { + embed(embed) + } + +/** + * Sends a follow-up in the command thread. + */ +suspend inline fun Responder.followUp(message: String): Responder.EditableResponse = followUp { + content = message +} + +/** + * Sends a follow-up in the command thread. + */ +suspend inline fun Responder.followUp(embed: EmbedBuilder): Responder.EditableResponse = followUp { + this.embed = embed +} + +/** + * Sends a follow-up in the command thread. + */ +suspend inline fun Responder.followUpEmbed(crossinline embed: EmbedBuilder.() -> Unit): Responder.EditableResponse = + followUp { + embed(embed) + } + +/** + * Edits the response. + */ +suspend inline fun Responder.EditableResponse.edit(message: String): Responder.EditableResponse = edit { + content = message +} + +/** + * Edits the response. + */ +suspend inline fun Responder.EditableResponse.edit(embed: EmbedBuilder): Responder.EditableResponse = + edit { + this.embed = embed + } + +/** + * Edits the response. + */ +suspend inline fun Responder.EditableResponse.edit(crossinline embed: EmbedBuilder.() -> Unit): Responder.EditableResponse = + edit { + embed(embed) + } diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/internal/CommandExecutor.kt b/src/main/kotlin/dev/schlaubi/votebot/command/internal/CommandExecutor.kt new file mode 100644 index 0000000..2586877 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/internal/CommandExecutor.kt @@ -0,0 +1,119 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.internal + +import dev.kord.core.behavior.interaction.respondEphemeral +import dev.kord.core.entity.interaction.GuildInteraction +import dev.kord.core.entity.interaction.SubCommand +import dev.kord.core.event.interaction.InteractionCreateEvent +import dev.kord.core.on +import dev.kord.rest.builder.interaction.ApplicationCommandsCreateBuilder +import dev.schlaubi.votebot.command.CommandErrorHandler +import dev.schlaubi.votebot.command.ExecutableCommand +import dev.schlaubi.votebot.command.RegistrableCommand +import dev.schlaubi.votebot.command.RootCommand +import dev.schlaubi.votebot.command.SingleCommand +import dev.schlaubi.votebot.command.context.response.EphemeralResponseStrategy +import dev.schlaubi.votebot.command.context.response.PublicResponseStrategy +import dev.schlaubi.votebot.command.context.response.Responder +import dev.schlaubi.votebot.config.Config +import dev.schlaubi.votebot.core.VoteBot +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch +import mu.KotlinLogging +import kotlin.coroutines.CoroutineContext + +private val LOG = KotlinLogging.logger { } + +class CommandExecutor( + private val bot: VoteBot, + val commands: Map, + private val errorHandler: CommandErrorHandler +) : CoroutineScope { + override val coroutineContext: CoroutineContext = bot.coroutineContext + SupervisorJob() + private val listener = bot.kord.on { handle() } + + private suspend fun InteractionCreateEvent.handle() { + val interactionCommand = interaction.command + if (interaction !is GuildInteraction) { + interaction.respondEphemeral("You cannot use this bot in your DMs") + return + } + + val command = commands[interactionCommand.rootName] ?: return + + val foundCommand: ExecutableCommand = if (command is RootCommand) { + val subCommand = (interactionCommand as? SubCommand) + ?: error("Registered command isn't sub command but implementation is. Class: ${command::class.qualifiedName}") + + command.subCommands[subCommand.name] + ?: error("Could not find subcommand: ${subCommand.name} for root ${interactionCommand.rootName}") + } else command as SingleCommand + + val responseStrategy: Responder = if (foundCommand.useEphemeral) { + EphemeralResponseStrategy(interaction.acknowledgeEphemeral()) + } else { + PublicResponseStrategy(interaction.ackowledgePublic()) + } + + val context = ContextImpl( + bot, + foundCommand, + responseStrategy, + this + ) + + val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> + LOG.error(throwable) { "Error whilst executing command" } + launch { + errorHandler.handleCommandError(context, coroutineContext, throwable) + } + } + + LOG.debug { "Command ${command.name} was executed by ${context.interaction.user.id}" } + launch(exceptionHandler) { + foundCommand.execute(context) + } + } + + suspend fun updateCommand() { + val commandRegisterer: ApplicationCommandsCreateBuilder.() -> Unit = { + this@CommandExecutor.commands.forEach { (_, command) -> + with(command) { + command(command.name, command.description) { + defaultPermission = command.defaultPermission + addArguments() + } + } + } + } + + if (Config.ENVIRONMENT.useGlobalCommands) { + bot.kord.slashCommands.createGlobalApplicationCommands(commandRegisterer).launchIn(bot) + } else { + bot.kord.slashCommands.createGuildApplicationCommands(Config.DEV_GUILD!!, commandRegisterer).launchIn(bot) + } + } + + fun close() = listener.cancel() +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/internal/ContextImpl.kt b/src/main/kotlin/dev/schlaubi/votebot/command/internal/ContextImpl.kt new file mode 100644 index 0000000..e6af42c --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/internal/ContextImpl.kt @@ -0,0 +1,40 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.internal + +import dev.kord.core.entity.interaction.GuildInteraction +import dev.kord.core.entity.interaction.OptionValue +import dev.kord.core.event.interaction.InteractionCreateEvent +import dev.schlaubi.votebot.command.ExecutableCommand +import dev.schlaubi.votebot.command.context.Context +import dev.schlaubi.votebot.command.context.response.Responder +import dev.schlaubi.votebot.core.VoteBot + +class ContextImpl( + override val bot: VoteBot, + override val command: ExecutableCommand, + val strategy: Responder, + private val event: InteractionCreateEvent +) : Responder by strategy, Context { + override val interaction: GuildInteraction + get() = event.interaction as GuildInteraction + + override fun optionalArgument(name: String): OptionValue<*>? = interaction.command.options[name] +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/internal/DebugErrorHandler.kt b/src/main/kotlin/dev/schlaubi/votebot/command/internal/DebugErrorHandler.kt new file mode 100644 index 0000000..9ed4dc5 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/internal/DebugErrorHandler.kt @@ -0,0 +1,37 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.internal + +import dev.schlaubi.votebot.command.CommandErrorHandler +import dev.schlaubi.votebot.command.context.Context +import dev.schlaubi.votebot.command.context.response.followUp +import mu.KotlinLogging +import kotlin.coroutines.CoroutineContext + +object DebugErrorHandler : CommandErrorHandler { + private val LOG = KotlinLogging.logger { } + override suspend fun handleCommandError( + context: Context, + coroutineContext: CoroutineContext, + throwable: Throwable + ) { + context.followUp("An error occurred whilst handling this command! Please view the logs for more: `${throwable::class.simpleName}${throwable.message}`") + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt new file mode 100644 index 0000000..10c248e --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt @@ -0,0 +1,54 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.commands + +import dev.kord.common.entity.Permission +import dev.schlaubi.votebot.command.SingleCommand +import dev.schlaubi.votebot.command.context.Context +import dev.schlaubi.votebot.command.context.response.respond +import dev.schlaubi.votebot.util.appendPermission +import dev.schlaubi.votebot.util.getCommands +import kotlinx.coroutines.flow.first + +object ClaimPermissionsCommand : SingleCommand() { + override val name: String = "claim-permissions" + override val useEphemeral: Boolean = true + override val description: String = + "Allows you to claim /permissions command permissions if you have ADMINISTRATOR permissions" + + override suspend fun execute(context: Context) { + if (Permission.Administrator !in context.executor.asMember().getPermissions()) { + context.respond("You need the `ADMINISTRATOR` permission to execute this command") + return + } + + val command = context.guild.getCommands().first { it.name == PermissionCommand.name } + + appendPermission( + command.id, + context.bot.kord, + context.guild.id + ) { + user(context.executor.id, true) + } + + context.respond("You now have access to /permissions") + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt new file mode 100644 index 0000000..1f38e48 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt @@ -0,0 +1,34 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.commands + +import dev.schlaubi.votebot.command.SingleCommand +import dev.schlaubi.votebot.command.context.Context +import dev.schlaubi.votebot.command.context.response.respond + +object InfoCommand : SingleCommand() { + override val description: String = "Displays basic information about the bot" + override val name: String = "info" + override val useEphemeral: Boolean = true + + override suspend fun execute(context: Context) { + context.respond("Coming soon :tm:") + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt new file mode 100644 index 0000000..db154b5 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt @@ -0,0 +1,121 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.commands + +import dev.kord.rest.builder.interaction.ApplicationCommandPermissionsModifyBuilder +import dev.kord.rest.builder.interaction.SubCommandBuilder +import dev.schlaubi.votebot.command.RootCommand +import dev.schlaubi.votebot.command.SubCommand +import dev.schlaubi.votebot.command.context.Context +import dev.schlaubi.votebot.command.context.response.respond +import dev.schlaubi.votebot.config.Config +import dev.schlaubi.votebot.util.addCommand +import dev.schlaubi.votebot.util.appendPermission +import dev.schlaubi.votebot.util.buildCommands +import kotlinx.coroutines.flow.firstOrNull + +object PermissionCommand : RootCommand(buildCommands { + addCommand(RoleCommand()) + addCommand(UserCommand()) +}) { + override val name: String = "permissions" + override val defaultPermission: Boolean = false + override val description: String = "Allows you to manage the bots permissions" + + class RoleCommand : SubCommand() { + override val name: String = "role" + override val useEphemeral: Boolean = true + override val description: String = "Allows you to manage the permissions of a role" + + override fun SubCommandBuilder.addArguments() { + commandArgument() + role("role", "The Role you want to change the permissions of") { + required = true + } + permissionArgument() + } + + override suspend fun execute(context: Context) { + doPermissions(context) { permission -> + role(context.role("role").id, permission) + } + } + } + + class UserCommand : SubCommand() { + override val name: String = "user" + override val useEphemeral: Boolean = true + override val description: String = "Allows you to manage the permissions of a user" + + override fun SubCommandBuilder.addArguments() { + commandArgument() + user("user", "The User you want to change the permissions of") { + required = true + } + permissionArgument() + } + + override suspend fun execute(context: Context) { + doPermissions(context) { permission -> + user(context.user("user").id, permission) + } + } + } +} + +private fun SubCommandBuilder.permissionArgument() { + boolean("permission", "Whether you want to allow using the command or not") { + required = true + } +} + +private fun SubCommandBuilder.commandArgument() { + string("command", "The command you want to change the permissions for") { + required = true + } +} + +suspend fun doPermissions( + context: Context, + addPermission: ApplicationCommandPermissionsModifyBuilder.(Boolean) -> Unit +) { + val commandName = context.string("command") + val commands = if (Config.ENVIRONMENT.useGlobalCommands) { + context.bot.kord.slashCommands.getGlobalApplicationCommands() + } else { + context.bot.kord.slashCommands.getGuildApplicationCommands(context.guild.id) + } + + val command = commands.firstOrNull { it.name == commandName } + if (command == null) { + context.respond("Unknown command") + return + } + + appendPermission( + command.id, + context.bot.kord, + context.guild.id + ) { + addPermission(context.boolean("permission")) + } + + context.respond("Permission was updated!") +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt b/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt index df67a2c..bef83b7 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt @@ -20,16 +20,70 @@ package dev.schlaubi.votebot.config import ch.qos.logback.classic.Level +import dev.kord.common.entity.Snowflake +import dev.schlaubi.envconf.environment import dev.schlaubi.envconf.getEnv +import dev.schlaubi.votebot.command.CommandErrorHandler +import dev.schlaubi.votebot.command.internal.DebugErrorHandler +/** + * Environment based config. + * + * **Note:** All Properties are resolved by looking up the environment variable with the same name as the property + */ object Config { + + /** + * The Discord bot token. + */ + val DISCORD_TOKEN by environment + + /** + * The id of the dev guild if using [Environment.DEVELOPMENT]. + */ + val DEV_GUILD by getEnv { Snowflake(it) }.optional() + + /** + * The Environment this instance runs in. + * + * @see Environment + */ val ENVIRONMENT by getEnv("", Environment.PRODUCTION, Environment::valueOf) + + /** + * The LOG level of the root logger. + */ val LOG_LEVEL: Level by getEnv(default = Level.INFO) { Level.toLevel(it) } + /** + * The Sentry DSN. + */ val SENTRY_TOKEN by getEnv().optional() } -enum class Environment(val useSentry: Boolean = true) { - PRODUCTION, - DEVELOPMENT(false) +/** + * Environmentally based settings. + * + * @property useGlobalCommands whether this should use only guild commands on [Config.DEV_GUILD] or global commands + * @property useSentry whether sentry error logging is enabled or not + * @property errorHandler the [CommandErrorHandler] used in this environment + */ +enum class Environment( + val useGlobalCommands: Boolean = true, + val useSentry: Boolean = true, + val errorHandler: CommandErrorHandler +) { + /** + * Production environment: + * - Global commands + * - Sentry error handling + */ + PRODUCTION(errorHandler = DebugErrorHandler), + + /** + * Development environment: + * - no sentry + * - guild commands on [Config.DEV_GUILD] + */ + DEVELOPMENT(false, false, DebugErrorHandler) } diff --git a/src/main/kotlin/dev/schlaubi/votebot/core/VoteBot.kt b/src/main/kotlin/dev/schlaubi/votebot/core/VoteBot.kt new file mode 100644 index 0000000..3966dde --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/core/VoteBot.kt @@ -0,0 +1,40 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.core + +import dev.kord.core.Kord +import dev.schlaubi.votebot.command.RegistrableCommand +import kotlinx.coroutines.CoroutineScope + +/** + * VoteBot monolith I guess? + */ +interface VoteBot : CoroutineScope { + + /** + * Kord. + */ + val kord: Kord + + /** + * A list of all registered commands. + */ + val commands: Map +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt b/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt new file mode 100644 index 0000000..15bbbc0 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt @@ -0,0 +1,58 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.core + +import dev.kord.core.Kord +import dev.schlaubi.votebot.command.RegistrableCommand +import dev.schlaubi.votebot.command.internal.CommandExecutor +import dev.schlaubi.votebot.commands.ClaimPermissionsCommand +import dev.schlaubi.votebot.commands.InfoCommand +import dev.schlaubi.votebot.commands.PermissionCommand +import dev.schlaubi.votebot.config.Config +import dev.schlaubi.votebot.util.addCommand +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext + +internal class VoteBotImpl : VoteBot { + override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob() + + override lateinit var kord: Kord + private lateinit var commandExecutor: CommandExecutor + override val commands: Map + get() = commandExecutor.commands + + suspend fun start() { + kord = Kord(Config.DISCORD_TOKEN) + commandExecutor = CommandExecutor( + this, + commands(), + Config.ENVIRONMENT.errorHandler + ) + commandExecutor.updateCommand() + kord.login() + } + + private fun commands() = buildMap { + addCommand(InfoCommand) + addCommand(PermissionCommand) + addCommand(ClaimPermissionsCommand) + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/CommandRegistrationUtil.kt b/src/main/kotlin/dev/schlaubi/votebot/util/CommandRegistrationUtil.kt new file mode 100644 index 0000000..f952626 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/util/CommandRegistrationUtil.kt @@ -0,0 +1,60 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.util + +import dev.kord.core.behavior.GuildBehavior +import dev.kord.core.entity.interaction.ApplicationCommand +import dev.schlaubi.votebot.command.DescriptiveCommand +import dev.schlaubi.votebot.config.Config +import dev.schlaubi.votebot.config.Environment +import kotlinx.coroutines.flow.Flow +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Retrieves the commands depending on [Environment.useGlobalCommands]. + */ +fun GuildBehavior.getCommands(): Flow = if (Config.ENVIRONMENT.useGlobalCommands) { + kord.slashCommands.getGlobalApplicationCommands() +} else { + kord.slashCommands.getGuildApplicationCommands(id) +} + +/** + * Builder for a command-name-map. + */ +@OptIn(ExperimentalContracts::class) +inline fun buildCommands(builder: MutableMap.() -> Unit): Map { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + return buildMap(builder) +} + +/** + * Adds a command to a command-name-map + * + * @see buildCommands + */ +fun MutableMap.addCommand(command: C) { + this[command.name] = command +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/VoteBot.kt b/src/main/kotlin/dev/schlaubi/votebot/util/EmbedUtils.kt similarity index 71% rename from src/main/kotlin/dev/schlaubi/votebot/VoteBot.kt rename to src/main/kotlin/dev/schlaubi/votebot/util/EmbedUtils.kt index d67c58e..27891c9 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/VoteBot.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/EmbedUtils.kt @@ -1,5 +1,5 @@ /* - * VoteBot - A feature-rich bot to create votes on Discord guilds. + * Votebot - A feature-rich bot to create votes on Discord guilds. * * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger * @@ -17,7 +17,13 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -package dev.schlaubi.votebot +package dev.schlaubi.votebot.util -class VoteBot { -} \ No newline at end of file +import dev.kord.rest.builder.message.EmbedBuilder + +/** + * Global scope builder for embeds. + * + * @see EmbedBuilder + */ +fun embed(builder: EmbedBuilder.() -> Unit) = EmbedBuilder().apply(builder) diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/OptionalUtil.kt b/src/main/kotlin/dev/schlaubi/votebot/util/OptionalUtil.kt new file mode 100644 index 0000000..fbf8a87 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/util/OptionalUtil.kt @@ -0,0 +1,27 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.util + +import dev.kord.common.entity.optional.Optional + +/** + * Turns a nullable item into an [Optional]. + */ +fun T?.optional(): Optional = if (this == null) Optional.Missing() else Optional.Value(this) diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/PermissionUtil.kt b/src/main/kotlin/dev/schlaubi/votebot/util/PermissionUtil.kt new file mode 100644 index 0000000..c06193b --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/util/PermissionUtil.kt @@ -0,0 +1,65 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.util + +import dev.kord.common.entity.DiscordGuildApplicationCommandPermission +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.rest.builder.interaction.ApplicationCommandPermissionsModifyBuilder +import dev.kord.rest.request.KtorRequestException +import kotlinx.coroutines.flow.toList +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Modifies the permissions of [id] without overwriting existing permissions. + * + * @param kord the [Kord] instance to send the request + * @param guildId the id of the guild to update the permissions on + */ +@OptIn(ExperimentalContracts::class) +suspend inline fun appendPermission( + id: Snowflake, + kord: Kord, + guildId: Snowflake, + crossinline builder: ApplicationCommandPermissionsModifyBuilder.() -> Unit +) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + val permissions = try { + kord.slashCommands.getApplicationCommandPermissions(kord.slashCommands.applicationId, guildId, id).permissions.toList() + } catch (e: KtorRequestException) { + emptyList() // Discord will error if the command doesn't have permissions yet + } + + kord.slashCommands.editApplicationCommandPermissions(kord.slashCommands.applicationId, guildId, id) { + this.permissions = permissions.map { + DiscordGuildApplicationCommandPermission( + it.id, + it.type, + it.permission + ) + }.toMutableList() + + builder(this) + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 3068423..eb3f4fc 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,9 +1,6 @@ - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n From 49db0403e6055d801eb79a7f28bfc2e4471910f0 Mon Sep 17 00:00:00 2001 From: schlaubi Date: Sun, 6 Jun 2021 20:36:49 +0200 Subject: [PATCH 04/12] Add Dockerfile - Run ktlint --- Dockerfile | 7 +++++++ .../context/response/EphemeralResponseStrategy.kt | 2 +- .../context/response/FollowUpResponseStrategy.kt | 2 +- .../command/context/response/PublicResponseStrategy.kt | 4 ++-- .../dev/schlaubi/votebot/commands/PermissionCommand.kt | 10 ++++++---- 5 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3ef56bd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM adoptopenjdk/openjdk16 + +WORKDIR /usr/app + +COPY build/install . + +ENTRYPOINT ["bin/votebot"] diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/EphemeralResponseStrategy.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/EphemeralResponseStrategy.kt index 23194e1..1963a86 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/EphemeralResponseStrategy.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/EphemeralResponseStrategy.kt @@ -48,4 +48,4 @@ internal class EphemeralResponseStrategy(private val ack: EphemeralInteractionRe @OptIn(KordUnsafe::class) override suspend fun internalFollowUp(builder: PublicFollowupMessageCreateBuilder.() -> Unit): PublicFollowupMessage = ack.followUp(builder) -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/FollowUpResponseStrategy.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/FollowUpResponseStrategy.kt index 5aec3aa..a4a3e28 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/FollowUpResponseStrategy.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/FollowUpResponseStrategy.kt @@ -59,4 +59,4 @@ internal sealed class FollowUpResponseStrategy : Responder { return this } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/PublicResponseStrategy.kt b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/PublicResponseStrategy.kt index 589fa7b..000eb76 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/command/context/response/PublicResponseStrategy.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/command/context/response/PublicResponseStrategy.kt @@ -29,7 +29,8 @@ import dev.kord.rest.builder.interaction.PublicFollowupMessageCreateBuilder import dev.kord.rest.builder.message.MessageCreateBuilder import dev.kord.rest.builder.message.MessageModifyBuilder -internal class PublicResponseStrategy(private val ack: PublicInteractionResponseBehavior) : FollowUpResponseStrategy(), +internal class PublicResponseStrategy(private val ack: PublicInteractionResponseBehavior) : + FollowUpResponseStrategy(), Responder { override suspend fun internalFollowUp(builder: PublicFollowupMessageCreateBuilder.() -> Unit): PublicFollowupMessage = ack.followUp(builder) @@ -50,7 +51,6 @@ internal class PublicResponseStrategy(private val ack: PublicInteractionResponse return MessageEditableResponse(response) } - private class MessageEditableResponse(private val sentMessage: Message) : Responder.EditableResponse { override suspend fun edit(builder: MessageModifyBuilder.() -> Unit): Responder.EditableResponse { val message = MessageModifyBuilder().apply(builder) diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt index db154b5..7537702 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt @@ -31,10 +31,12 @@ import dev.schlaubi.votebot.util.appendPermission import dev.schlaubi.votebot.util.buildCommands import kotlinx.coroutines.flow.firstOrNull -object PermissionCommand : RootCommand(buildCommands { - addCommand(RoleCommand()) - addCommand(UserCommand()) -}) { +object PermissionCommand : RootCommand( + buildCommands { + addCommand(RoleCommand()) + addCommand(UserCommand()) + } +) { override val name: String = "permissions" override val defaultPermission: Boolean = false override val description: String = "Allows you to manage the bots permissions" From 88f94a4496f6a35c7ea5761f71d4eb08cbf6514c Mon Sep 17 00:00:00 2001 From: schlaubi Date: Sun, 6 Jun 2021 20:48:14 +0200 Subject: [PATCH 05/12] Optimize CI --- .github/workflows/votebot.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/votebot.yml b/.github/workflows/votebot.yml index 97f1fd5..9207d24 100644 --- a/.github/workflows/votebot.yml +++ b/.github/workflows/votebot.yml @@ -19,16 +19,23 @@ jobs: - name: Test with Gradle run: ./gradlew test ktlintCheck - name: Build with Gradle + run: ./gradlew classes + - name: Make Distribution with Gradle + if: github.ref == 'refs/heads/main' run: ./gradlew installDist - name: Login + if: github.ref == 'refs/heads/main' env: GITHUB_TOKEN: ${{ secrets.DOCKER_TOKEN }} - run: docker login ghcr.io --username ci --password "$GITHUB_TOKEN" + run: docker login ghcr.io --username ci --password "$GITHUB_TOKEN" - name: Build & Tag - run: docker build -t ghcr.io/votebot/votebot/bot:latest -t ghcr.io/votebot/votebot/bot:"$GITHUB_SHA" . + if: github.ref == 'refs/heads/main' + run: docker build -t ghcr.io/votebot/votebot/bot:latest -t ghcr.io/votebot/votebot/bot:"$GITHUB_SHA" . - name: Push - run: docker push ghcr.io/votebot/votebot/bot:latest + if: github.ref == 'refs/heads/main' + run: docker push ghcr.io/votebot/votebot/bot:latest - name: Push specific tag + if: github.ref == 'refs/heads/main' run: docker push ghcr.io/votebot/votebot/bot:"$GITHUB_SHA" - name: Create Sentry Release if: github.ref == 'refs/heads/main' From e0c769347197c42cb444ffe9f91d2c10e28c7ec9 Mon Sep 17 00:00:00 2001 From: niggelgame Date: Sun, 6 Jun 2021 22:25:57 +0200 Subject: [PATCH 06/12] feat: add embed builder and migrate current commands to embeds fix: change code style to pass style test fix: remove comments and use global embed function feat: add new branding colors --- .gitignore | 3 + .idea/codeStyles/Project.xml | 10 ++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/modules.xml | 8 + .idea/votebot.iml | 12 ++ .../commands/ClaimPermissionsCommand.kt | 5 +- .../schlaubi/votebot/commands/InfoCommand.kt | 3 +- .../votebot/commands/PermissionCommand.kt | 5 +- .../dev/schlaubi/votebot/util/Colors.kt | 49 ++++++ .../dev/schlaubi/votebot/util/Embeds.kt | 145 ++++++++++++++++++ .../dev/schlaubi/votebot/util/Emotes.kt | 36 +++++ 11 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/votebot.iml create mode 100644 src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt diff --git a/.gitignore b/.gitignore index a01c2fd..0f43b23 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ gradle-app.setting **/build/ # End of https://www.toptal.com/developers/gitignore/api/kotlin,gradle + + +.env \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..1bec35e --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a2e2410 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/votebot.iml b/.idea/votebot.iml new file mode 100644 index 0000000..f100729 --- /dev/null +++ b/.idea/votebot.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt index 10c248e..4aae022 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt @@ -23,6 +23,7 @@ import dev.kord.common.entity.Permission import dev.schlaubi.votebot.command.SingleCommand import dev.schlaubi.votebot.command.context.Context import dev.schlaubi.votebot.command.context.response.respond +import dev.schlaubi.votebot.util.Embeds import dev.schlaubi.votebot.util.appendPermission import dev.schlaubi.votebot.util.getCommands import kotlinx.coroutines.flow.first @@ -35,7 +36,7 @@ object ClaimPermissionsCommand : SingleCommand() { override suspend fun execute(context: Context) { if (Permission.Administrator !in context.executor.asMember().getPermissions()) { - context.respond("You need the `ADMINISTRATOR` permission to execute this command") + context.respond(Embeds.error("You need the `ADMINISTRATOR` permission to execute this command")) return } @@ -49,6 +50,6 @@ object ClaimPermissionsCommand : SingleCommand() { user(context.executor.id, true) } - context.respond("You now have access to /permissions") + context.respond(Embeds.info("You now have access to /permissions")) } } diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt index 1f38e48..434204e 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt @@ -22,6 +22,7 @@ package dev.schlaubi.votebot.commands import dev.schlaubi.votebot.command.SingleCommand import dev.schlaubi.votebot.command.context.Context import dev.schlaubi.votebot.command.context.response.respond +import dev.schlaubi.votebot.util.Embeds object InfoCommand : SingleCommand() { override val description: String = "Displays basic information about the bot" @@ -29,6 +30,6 @@ object InfoCommand : SingleCommand() { override val useEphemeral: Boolean = true override suspend fun execute(context: Context) { - context.respond("Coming soon :tm:") + context.respond(Embeds.info("Coming soon :tm:")) } } diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt index 7537702..25f0a58 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt @@ -26,6 +26,7 @@ import dev.schlaubi.votebot.command.SubCommand import dev.schlaubi.votebot.command.context.Context import dev.schlaubi.votebot.command.context.response.respond import dev.schlaubi.votebot.config.Config +import dev.schlaubi.votebot.util.Embeds import dev.schlaubi.votebot.util.addCommand import dev.schlaubi.votebot.util.appendPermission import dev.schlaubi.votebot.util.buildCommands @@ -107,7 +108,7 @@ suspend fun doPermissions( val command = commands.firstOrNull { it.name == commandName } if (command == null) { - context.respond("Unknown command") + context.respond(Embeds.error("Unknown command")) return } @@ -119,5 +120,5 @@ suspend fun doPermissions( addPermission(context.boolean("permission")) } - context.respond("Permission was updated!") + context.respond(Embeds.success("Permission was updated!")) } diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt new file mode 100644 index 0000000..3c7f8eb --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt @@ -0,0 +1,49 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.util + +import dev.kord.common.Color + +/** + * Wrapper for [Discordapp.com/branding][https://discordapp.com/branding] colors and some other colors: + */ +@Suppress("KDocMissingDocumentation", "unused", "MagicNumber") +object Colors { + // Discord + val BLURLPLE: Color = Color(88, 101, 242) + val GREEN: Color = Color(87, 242, 135) + val YELLOW: Color = Color(254, 231, 92) + val FUCHSIA: Color = Color(235, 69, 158) + val RED: Color = Color(237, 66, 69) + val WHITE: Color = Color(255, 255, 255) + val BLACK: Color = Color(0, 0, 0) + + // Old Discord Branding Colors + val GREYPLE: Color = Color(153, 170, 181) + val DARK_BUT_NOT_BLACK: Color = Color(44, 47, 51) + val NOT_QUITE_BLACK: Color = Color(33, 39, 42) + + // Other colors + val LIGHT_RED: Color = Color(231, 76, 60) + val DARK_RED: Color = Color(192, 57, 43) + val LIGHT_GREEN: Color = Color(46, 204, 113) + val DARK_GREEN: Color = Color(39, 174, 96) + val BLUE: Color = Color(52, 152, 219) +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt new file mode 100644 index 0000000..30b5242 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt @@ -0,0 +1,145 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.util + +import dev.kord.core.behavior.MessageBehavior +import dev.kord.core.behavior.channel.MessageChannelBehavior +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.edit +import dev.kord.core.entity.Message +import dev.kord.rest.builder.message.EmbedBuilder + +/** + * Defines a creator of an embed. + */ +typealias EmbedCreator = EmbedBuilder.() -> Unit + +/** + * Some presets for frequently used embeds. + */ +@Suppress("unused", "TooManyFunctions") +object Embeds { + + /** + * Creates a info embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun info(title: String, description: String? = null, builder: EmbedCreator = {}): EmbedBuilder = + embed { + title(Emotes.INFO, title) + this.description = description + color = Colors.BLUE + }.apply(builder) + + /** + * Creates a success embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun success( + title: String, + description: String? = null, + builder: EmbedCreator = {} + ): EmbedBuilder = + embed { + title(Emotes.SUCCESS, title) + this.description = description + color = Colors.GREEN + }.apply(builder) + + /** + * Creates a error embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun error( + title: String, + description: String? = null, + builder: EmbedCreator = {} + ): EmbedBuilder = + embed { + title(Emotes.ERROR, title) + this.description = description + color = Colors.RED + }.apply(builder) + + /** + * Creates a warning embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun warn( + title: String, + description: String? = null, + builder: EmbedCreator = {} + ): EmbedBuilder = + embed { + title(Emotes.WARN, title) + this.description = description + color = Colors.YELLOW + }.apply(builder) + + /** + * Creates a loading embed with the given [title] and [description] and applies the [builder] to it. + * @see EmbedCreator + * @see EmbedBuilder + */ + fun loading( + title: String, + description: String?, + builder: EmbedCreator = {} + ): EmbedBuilder = + embed { + title(Emotes.LOADING, title) + this.description = description + color = Colors.DARK_BUT_NOT_BLACK + }.apply(builder) + + private fun EmbedBuilder.title(emote: String, title: String) { + this.title = "$emote $title" + } + + /** + * Sends a new message in this channel containing the embed provided by [base] and applies [creator] to it + */ + suspend fun MessageChannelBehavior.createEmbed( + base: EmbedBuilder, + creator: suspend EmbedBuilder.() -> Unit = {} + ): Message { + return createMessage { + creator(base) + embed = base + } + } + + /** + * Sends a new message in this channel containing the embed provided by [base] and applies [creator] to it + */ + suspend fun MessageBehavior.editEmbed( + base: EmbedBuilder, + creator: suspend EmbedBuilder.() -> Unit = {} + ): Message { + return edit { + creator(base) + embed = base + } + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt new file mode 100644 index 0000000..ae86d26 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt @@ -0,0 +1,36 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.util + +/** + * Useful collection of Discord emotes. + * + * + * Designed by [Rxsto#1337](https://rxsto.me) + * Bot needs to be on [https://discord.gg/8phqcej](https://discord.gg/8phqcej) + */ +@Suppress("KDocMissingDocumentation") +object Emotes { + const val LOADING: String = "" + const val ERROR: String = "<:error:535827110489620500>" + const val WARN: String = "<:warn:535832532365737987>" + const val INFO: String = "<:info:535828529573789696>" + const val SUCCESS: String = "<:success:535827110552666112>" +} From a5b3a6c5978dc924480c2341cbe64f8b32ea37cb Mon Sep 17 00:00:00 2001 From: Nicolas Edelmann Date: Sun, 6 Jun 2021 22:01:16 +0200 Subject: [PATCH 07/12] feat: add embed builder and migrate current commands to embeds --- .../commands/ClaimPermissionsCommand.kt | 2 +- .../schlaubi/votebot/commands/InfoCommand.kt | 1 + .../votebot/commands/PermissionCommand.kt | 2 +- .../dev/schlaubi/votebot/core/VoteBotImpl.kt | 1 + .../dev/schlaubi/votebot/util/Colors.kt | 14 +-- .../dev/schlaubi/votebot/util/Embeds.kt | 86 +++++++++++++++++-- .../dev/schlaubi/votebot/util/Emotes.kt | 2 +- 7 files changed, 86 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt index 4aae022..53bc092 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt @@ -36,7 +36,7 @@ object ClaimPermissionsCommand : SingleCommand() { override suspend fun execute(context: Context) { if (Permission.Administrator !in context.executor.asMember().getPermissions()) { - context.respond(Embeds.error("You need the `ADMINISTRATOR` permission to execute this command")) + context.respond(Embeds.error("You need the `ADMINISTRATOR` permission to execute this command", description = null)) return } diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt index 434204e..e93dd4b 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt @@ -22,6 +22,7 @@ package dev.schlaubi.votebot.commands import dev.schlaubi.votebot.command.SingleCommand import dev.schlaubi.votebot.command.context.Context import dev.schlaubi.votebot.command.context.response.respond +import dev.schlaubi.votebot.command.context.response.respondEmbed import dev.schlaubi.votebot.util.Embeds object InfoCommand : SingleCommand() { diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt index 25f0a58..2e7365a 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt @@ -108,7 +108,7 @@ suspend fun doPermissions( val command = commands.firstOrNull { it.name == commandName } if (command == null) { - context.respond(Embeds.error("Unknown command")) + context.respond(Embeds.error("Unknown command", description = null)) return } diff --git a/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt b/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt index 15bbbc0..a047cae 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt @@ -19,6 +19,7 @@ package dev.schlaubi.votebot.core +import dev.kord.common.entity.PresenceStatus import dev.kord.core.Kord import dev.schlaubi.votebot.command.RegistrableCommand import dev.schlaubi.votebot.command.internal.CommandExecutor diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt index 3c7f8eb..b0b0642 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt @@ -27,15 +27,8 @@ import dev.kord.common.Color @Suppress("KDocMissingDocumentation", "unused", "MagicNumber") object Colors { // Discord - val BLURLPLE: Color = Color(88, 101, 242) - val GREEN: Color = Color(87, 242, 135) - val YELLOW: Color = Color(254, 231, 92) - val FUCHSIA: Color = Color(235, 69, 158) - val RED: Color = Color(237, 66, 69) - val WHITE: Color = Color(255, 255, 255) - val BLACK: Color = Color(0, 0, 0) - - // Old Discord Branding Colors + val BLURLPLE: Color = Color(114, 137, 218) + val FULL_WHITE: Color = Color(255, 255, 255) val GREYPLE: Color = Color(153, 170, 181) val DARK_BUT_NOT_BLACK: Color = Color(44, 47, 51) val NOT_QUITE_BLACK: Color = Color(33, 39, 42) @@ -46,4 +39,5 @@ object Colors { val LIGHT_GREEN: Color = Color(46, 204, 113) val DARK_GREEN: Color = Color(39, 174, 96) val BLUE: Color = Color(52, 152, 219) -} + val YELLOW: Color = Color(241, 196, 15) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt index 30b5242..727e78e 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt @@ -21,10 +21,11 @@ package dev.schlaubi.votebot.util import dev.kord.core.behavior.MessageBehavior import dev.kord.core.behavior.channel.MessageChannelBehavior -import dev.kord.core.behavior.channel.createMessage import dev.kord.core.behavior.edit import dev.kord.core.entity.Message import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.core.behavior.channel.createMessage + /** * Defines a creator of an embed. @@ -43,7 +44,7 @@ object Embeds { * @see EmbedBuilder */ fun info(title: String, description: String? = null, builder: EmbedCreator = {}): EmbedBuilder = - embed { + EmbedBuilder().apply { title(Emotes.INFO, title) this.description = description color = Colors.BLUE @@ -59,10 +60,10 @@ object Embeds { description: String? = null, builder: EmbedCreator = {} ): EmbedBuilder = - embed { + EmbedBuilder().apply { title(Emotes.SUCCESS, title) this.description = description - color = Colors.GREEN + color = Colors.LIGHT_GREEN }.apply(builder) /** @@ -75,10 +76,10 @@ object Embeds { description: String? = null, builder: EmbedCreator = {} ): EmbedBuilder = - embed { + EmbedBuilder().apply { title(Emotes.ERROR, title) this.description = description - color = Colors.RED + color = Colors.LIGHT_RED }.apply(builder) /** @@ -91,7 +92,7 @@ object Embeds { description: String? = null, builder: EmbedCreator = {} ): EmbedBuilder = - embed { + EmbedBuilder().apply { title(Emotes.WARN, title) this.description = description color = Colors.YELLOW @@ -107,12 +108,62 @@ object Embeds { description: String?, builder: EmbedCreator = {} ): EmbedBuilder = - embed { + EmbedBuilder().apply { title(Emotes.LOADING, title) this.description = description color = Colors.DARK_BUT_NOT_BLACK }.apply(builder) +/* + */ + /** + * Creates a help embed for [command]. + *//* + fun command(command: DescriptiveCommand, processor: CommandProcessor): EmbedBuilder { + val children = + processor.commands.filterValues { (it.aliasInfo as? AliasInfo.Child<*>)?.parent == command }.keys + return info("${command.name} - Hilfe", command.description) { + if (children.isNotEmpty()) { + field { + name = "Aliase" + value = children.joinToString("`, `", "`", "`") + } + } + field { + name = "Usage" + value = formatCommandUsage(command) + } + field { + name = "Permission" + value = command.permission.toString() + } +// addField("Permission", command.permission.name) +// val subCommands = command.registeredCommands.map(::formatSubCommandUsage) +// if (subCommands.isNotEmpty()) { +// addField("Sub commands", subCommands.joinToString("\n")) +// } + } + }*/ + + /*private fun formatCommandUsage(command: Command<*>): String { + val usage = command.arguments.joinToString(" ") { + if ("optional" in it.toString()) "[${it.name}]" else "<${it.name}>" + } + + return "!${command.name} $usage" + }*/ +// +// private fun formatSubCommandUsage(command: AbstractSubCommand): String { +// val builder = StringBuilder(Constants.firstPrefix) +// builder.append(' ').append(command.name).append(' ').append(command.usage.replace("\n", "\\n")) +// +// val prefix = " ${command.parent.name} " +// builder.insert(Constants.firstPrefix.length, prefix) +// builder.append(" - ").append(command.description) +// +// return builder.toString() +// } + private fun EmbedBuilder.title(emote: String, title: String) { this.title = "$emote $title" } @@ -142,4 +193,21 @@ object Embeds { embed = base } } -} + + +/* + */ + /** + * Responds to this command with an embed provided by [base] and applies [creator] to it + *//* + @Deprecated("Use sendResponse instead", ReplaceWith("sendResponse(base, creator)")) + suspend fun KordEvent.respondEmbed( + base: EmbedBuilder, + creator: suspend EmbedBuilder.() -> Unit = {} + ): Message { + return respond { + creator(base) + embed = base + } + }*/ +} \ No newline at end of file diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt index ae86d26..28e9f14 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt @@ -33,4 +33,4 @@ object Emotes { const val WARN: String = "<:warn:535832532365737987>" const val INFO: String = "<:info:535828529573789696>" const val SUCCESS: String = "<:success:535827110552666112>" -} +} \ No newline at end of file From 853e3c2d5100d5f7f6b3bf7979866e6b2bc3c097 Mon Sep 17 00:00:00 2001 From: Nicolas Edelmann Date: Sun, 6 Jun 2021 22:09:54 +0200 Subject: [PATCH 08/12] fix: change code style to pass style test --- .../kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt | 1 - src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt | 1 - src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt | 2 +- src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt | 6 ++---- src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt index e93dd4b..434204e 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.kt @@ -22,7 +22,6 @@ package dev.schlaubi.votebot.commands import dev.schlaubi.votebot.command.SingleCommand import dev.schlaubi.votebot.command.context.Context import dev.schlaubi.votebot.command.context.response.respond -import dev.schlaubi.votebot.command.context.response.respondEmbed import dev.schlaubi.votebot.util.Embeds object InfoCommand : SingleCommand() { diff --git a/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt b/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt index a047cae..15bbbc0 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/core/VoteBotImpl.kt @@ -19,7 +19,6 @@ package dev.schlaubi.votebot.core -import dev.kord.common.entity.PresenceStatus import dev.kord.core.Kord import dev.schlaubi.votebot.command.RegistrableCommand import dev.schlaubi.votebot.command.internal.CommandExecutor diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt index b0b0642..412928e 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt @@ -40,4 +40,4 @@ object Colors { val DARK_GREEN: Color = Color(39, 174, 96) val BLUE: Color = Color(52, 152, 219) val YELLOW: Color = Color(241, 196, 15) -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt index 727e78e..737ed03 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt @@ -21,11 +21,10 @@ package dev.schlaubi.votebot.util import dev.kord.core.behavior.MessageBehavior import dev.kord.core.behavior.channel.MessageChannelBehavior +import dev.kord.core.behavior.channel.createMessage import dev.kord.core.behavior.edit import dev.kord.core.entity.Message import dev.kord.rest.builder.message.EmbedBuilder -import dev.kord.core.behavior.channel.createMessage - /** * Defines a creator of an embed. @@ -194,7 +193,6 @@ object Embeds { } } - /* */ /** @@ -210,4 +208,4 @@ object Embeds { embed = base } }*/ -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt index 28e9f14..ae86d26 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Emotes.kt @@ -33,4 +33,4 @@ object Emotes { const val WARN: String = "<:warn:535832532365737987>" const val INFO: String = "<:info:535828529573789696>" const val SUCCESS: String = "<:success:535827110552666112>" -} \ No newline at end of file +} From 1cbbd8ae796cfe9cbbae0255b7ee589075235c80 Mon Sep 17 00:00:00 2001 From: Nicolas Edelmann Date: Sun, 6 Jun 2021 22:14:42 +0200 Subject: [PATCH 09/12] fix: remove comments and use global embed function --- .../commands/ClaimPermissionsCommand.kt | 2 +- .../votebot/commands/PermissionCommand.kt | 2 +- .../dev/schlaubi/votebot/util/Embeds.kt | 76 ++----------------- 3 files changed, 7 insertions(+), 73 deletions(-) diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt index 53bc092..4aae022 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt @@ -36,7 +36,7 @@ object ClaimPermissionsCommand : SingleCommand() { override suspend fun execute(context: Context) { if (Permission.Administrator !in context.executor.asMember().getPermissions()) { - context.respond(Embeds.error("You need the `ADMINISTRATOR` permission to execute this command", description = null)) + context.respond(Embeds.error("You need the `ADMINISTRATOR` permission to execute this command")) return } diff --git a/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt index 2e7365a..25f0a58 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt @@ -108,7 +108,7 @@ suspend fun doPermissions( val command = commands.firstOrNull { it.name == commandName } if (command == null) { - context.respond(Embeds.error("Unknown command", description = null)) + context.respond(Embeds.error("Unknown command")) return } diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt index 737ed03..a128d2b 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt @@ -43,7 +43,7 @@ object Embeds { * @see EmbedBuilder */ fun info(title: String, description: String? = null, builder: EmbedCreator = {}): EmbedBuilder = - EmbedBuilder().apply { + embed { title(Emotes.INFO, title) this.description = description color = Colors.BLUE @@ -59,7 +59,7 @@ object Embeds { description: String? = null, builder: EmbedCreator = {} ): EmbedBuilder = - EmbedBuilder().apply { + embed { title(Emotes.SUCCESS, title) this.description = description color = Colors.LIGHT_GREEN @@ -75,7 +75,7 @@ object Embeds { description: String? = null, builder: EmbedCreator = {} ): EmbedBuilder = - EmbedBuilder().apply { + embed { title(Emotes.ERROR, title) this.description = description color = Colors.LIGHT_RED @@ -91,7 +91,7 @@ object Embeds { description: String? = null, builder: EmbedCreator = {} ): EmbedBuilder = - EmbedBuilder().apply { + embed { title(Emotes.WARN, title) this.description = description color = Colors.YELLOW @@ -107,62 +107,12 @@ object Embeds { description: String?, builder: EmbedCreator = {} ): EmbedBuilder = - EmbedBuilder().apply { + embed { title(Emotes.LOADING, title) this.description = description color = Colors.DARK_BUT_NOT_BLACK }.apply(builder) -/* - */ - /** - * Creates a help embed for [command]. - *//* - fun command(command: DescriptiveCommand, processor: CommandProcessor): EmbedBuilder { - val children = - processor.commands.filterValues { (it.aliasInfo as? AliasInfo.Child<*>)?.parent == command }.keys - return info("${command.name} - Hilfe", command.description) { - if (children.isNotEmpty()) { - field { - name = "Aliase" - value = children.joinToString("`, `", "`", "`") - } - } - field { - name = "Usage" - value = formatCommandUsage(command) - } - field { - name = "Permission" - value = command.permission.toString() - } -// addField("Permission", command.permission.name) -// val subCommands = command.registeredCommands.map(::formatSubCommandUsage) -// if (subCommands.isNotEmpty()) { -// addField("Sub commands", subCommands.joinToString("\n")) -// } - } - }*/ - - /*private fun formatCommandUsage(command: Command<*>): String { - val usage = command.arguments.joinToString(" ") { - if ("optional" in it.toString()) "[${it.name}]" else "<${it.name}>" - } - - return "!${command.name} $usage" - }*/ -// -// private fun formatSubCommandUsage(command: AbstractSubCommand): String { -// val builder = StringBuilder(Constants.firstPrefix) -// builder.append(' ').append(command.name).append(' ').append(command.usage.replace("\n", "\\n")) -// -// val prefix = " ${command.parent.name} " -// builder.insert(Constants.firstPrefix.length, prefix) -// builder.append(" - ").append(command.description) -// -// return builder.toString() -// } - private fun EmbedBuilder.title(emote: String, title: String) { this.title = "$emote $title" } @@ -192,20 +142,4 @@ object Embeds { embed = base } } - -/* - */ - /** - * Responds to this command with an embed provided by [base] and applies [creator] to it - *//* - @Deprecated("Use sendResponse instead", ReplaceWith("sendResponse(base, creator)")) - suspend fun KordEvent.respondEmbed( - base: EmbedBuilder, - creator: suspend EmbedBuilder.() -> Unit = {} - ): Message { - return respond { - creator(base) - embed = base - } - }*/ } From 1887699621cff23273d3a124f01453513dc3e428 Mon Sep 17 00:00:00 2001 From: Nicolas Edelmann Date: Sun, 6 Jun 2021 22:20:07 +0200 Subject: [PATCH 10/12] feat: add new branding colors --- src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt | 12 +++++++++--- src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt index 412928e..3c7f8eb 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Colors.kt @@ -27,8 +27,15 @@ import dev.kord.common.Color @Suppress("KDocMissingDocumentation", "unused", "MagicNumber") object Colors { // Discord - val BLURLPLE: Color = Color(114, 137, 218) - val FULL_WHITE: Color = Color(255, 255, 255) + val BLURLPLE: Color = Color(88, 101, 242) + val GREEN: Color = Color(87, 242, 135) + val YELLOW: Color = Color(254, 231, 92) + val FUCHSIA: Color = Color(235, 69, 158) + val RED: Color = Color(237, 66, 69) + val WHITE: Color = Color(255, 255, 255) + val BLACK: Color = Color(0, 0, 0) + + // Old Discord Branding Colors val GREYPLE: Color = Color(153, 170, 181) val DARK_BUT_NOT_BLACK: Color = Color(44, 47, 51) val NOT_QUITE_BLACK: Color = Color(33, 39, 42) @@ -39,5 +46,4 @@ object Colors { val LIGHT_GREEN: Color = Color(46, 204, 113) val DARK_GREEN: Color = Color(39, 174, 96) val BLUE: Color = Color(52, 152, 219) - val YELLOW: Color = Color(241, 196, 15) } diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt index a128d2b..30b5242 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/Embeds.kt @@ -62,7 +62,7 @@ object Embeds { embed { title(Emotes.SUCCESS, title) this.description = description - color = Colors.LIGHT_GREEN + color = Colors.GREEN }.apply(builder) /** @@ -78,7 +78,7 @@ object Embeds { embed { title(Emotes.ERROR, title) this.description = description - color = Colors.LIGHT_RED + color = Colors.RED }.apply(builder) /** From 38dd847bc8d4ddf8a6910730b17e004a63f24159 Mon Sep 17 00:00:00 2001 From: NyCode Date: Mon, 7 Jun 2021 19:54:20 +0200 Subject: [PATCH 11/12] Add production error handler --- .../{ => errorhandling}/DebugErrorHandler.kt | 2 +- .../ErrorInformationCollector.kt | 73 +++++++++++++++++++ .../errorhandling/ProductionErrorHandler.kt | 67 +++++++++++++++++ .../dev/schlaubi/votebot/config/Config.kt | 5 +- .../dev/schlaubi/votebot/util/SentryUtil.kt | 28 +++++++ 5 files changed, 172 insertions(+), 3 deletions(-) rename src/main/kotlin/dev/schlaubi/votebot/command/internal/{ => errorhandling}/DebugErrorHandler.kt (96%) create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/ErrorInformationCollector.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/ProductionErrorHandler.kt create mode 100644 src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/internal/DebugErrorHandler.kt b/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/DebugErrorHandler.kt similarity index 96% rename from src/main/kotlin/dev/schlaubi/votebot/command/internal/DebugErrorHandler.kt rename to src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/DebugErrorHandler.kt index 9ed4dc5..6c82d1a 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/command/internal/DebugErrorHandler.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/DebugErrorHandler.kt @@ -17,7 +17,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -package dev.schlaubi.votebot.command.internal +package dev.schlaubi.votebot.command.internal.errorhandling import dev.schlaubi.votebot.command.CommandErrorHandler import dev.schlaubi.votebot.command.context.Context diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/ErrorInformationCollector.kt b/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/ErrorInformationCollector.kt new file mode 100644 index 0000000..14a1188 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/ErrorInformationCollector.kt @@ -0,0 +1,73 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.internal.errorhandling + +import dev.schlaubi.votebot.command.context.Context +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import kotlin.coroutines.CoroutineContext + +object ErrorInformationCollector { + + /** + * Collects error information and returns them in a formatted way. + * @param context the current command [Context] + * @param coroutineContext the current [CoroutineContext] + * @param throwable the thrown [Throwable] + * @param thread the current [Thread] + * @see DebugErrorHandler + * @see ProductionErrorHandler + */ + suspend fun collectErrorInformation( + context: Context, + coroutineContext: CoroutineContext, + throwable: Throwable, + thread: Thread + ): String = coroutineScope { + val kord = context.kord + val guild = async { context.guild.asGuild() } + val executor = async { context.executor.asMember() } + val selfMember = async { guild.await().getMember(kord.selfId) } + val channel = async { context.interaction.channel.asChannel() } + val command = context.interaction.command + + return@coroutineScope """ + Command: ${command.rootName}(${command.rootId}) + Command Arguments: + ${command.options} + + Guild: ${guild.await().let { "${it.name}(${it.id})" }} + Executor: @${executor.await().let { "${it.tag}(${it.id.value})" }} + Permissions: ${selfMember.await().getPermissions().code} + + TextChannel: #${channel.await().let { "${it.name}(${it.id})" }} + Channel Permissions: ${channel.await().getEffectivePermissions(selfMember.await().id).code} + + Timestamp: ${Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())} + Thread:$thread + Coroutine: $coroutineContext + Stacktrace: + ${throwable.stackTraceToString()} + """.trimIndent() + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/ProductionErrorHandler.kt b/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/ProductionErrorHandler.kt new file mode 100644 index 0000000..bdf0139 --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/ProductionErrorHandler.kt @@ -0,0 +1,67 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.command.internal.errorhandling + +import dev.schlaubi.votebot.command.CommandErrorHandler +import dev.schlaubi.votebot.command.context.Context +import dev.schlaubi.votebot.command.context.response.followUp +import dev.schlaubi.votebot.config.Config +import dev.schlaubi.votebot.util.Embeds +import dev.schlaubi.votebot.util.whenUsingSentry +import io.sentry.Sentry +import io.sentry.SentryLevel +import kotlin.coroutines.CoroutineContext + +object ProductionErrorHandler : CommandErrorHandler { + + private val errorResponse = Embeds.error( + "Unexpected Error", + mutableListOf( + ":x: | **An unexpected error has occurred!**", + ).also { + if (Config.ENVIRONMENT.useSentry) { + it.addAll( + listOf( + "", + "> Error information will be collected", + "> and reported to the Development Team!" + ) + ) + } + }.joinToString("\n") + ) + + override suspend fun handleCommandError( + context: Context, + coroutineContext: CoroutineContext, + throwable: Throwable + ) { + context.followUp(errorResponse) + whenUsingSentry { + val errorInformation = ErrorInformationCollector.collectErrorInformation( + context, + coroutineContext, + throwable, + Thread.currentThread() + ) + Sentry.captureMessage(errorInformation, SentryLevel.ERROR) + } + } +} diff --git a/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt b/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt index bef83b7..fa2f13d 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt @@ -24,7 +24,8 @@ import dev.kord.common.entity.Snowflake import dev.schlaubi.envconf.environment import dev.schlaubi.envconf.getEnv import dev.schlaubi.votebot.command.CommandErrorHandler -import dev.schlaubi.votebot.command.internal.DebugErrorHandler +import dev.schlaubi.votebot.command.internal.errorhandling.DebugErrorHandler +import dev.schlaubi.votebot.command.internal.errorhandling.ProductionErrorHandler /** * Environment based config. @@ -78,7 +79,7 @@ enum class Environment( * - Global commands * - Sentry error handling */ - PRODUCTION(errorHandler = DebugErrorHandler), + PRODUCTION(errorHandler = ProductionErrorHandler), /** * Development environment: diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt b/src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt new file mode 100644 index 0000000..50c8d5d --- /dev/null +++ b/src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt @@ -0,0 +1,28 @@ +/* + * Votebot - A feature-rich bot to create votes on Discord guilds. + * + * Copyright (C) 2019-2021 Michael Rittmeister & Yannick Seeger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +package dev.schlaubi.votebot.util + +import dev.schlaubi.votebot.config.Config + +suspend fun whenUsingSentry(block: suspend () -> T): T? { + return if (Config.ENVIRONMENT.useSentry) + block() + else null +} From 0a88259a1117b6ce7a3622bee687a761f3d33a14 Mon Sep 17 00:00:00 2001 From: NyCode Date: Mon, 7 Jun 2021 20:58:54 +0200 Subject: [PATCH 12/12] Make whenUsingSentry inline and add contract --- src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt b/src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt index 50c8d5d..3b5a111 100644 --- a/src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt +++ b/src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt @@ -20,8 +20,15 @@ package dev.schlaubi.votebot.util import dev.schlaubi.votebot.config.Config +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract -suspend fun whenUsingSentry(block: suspend () -> T): T? { +@OptIn(ExperimentalContracts::class) +inline fun whenUsingSentry(block: () -> T): T? { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } return if (Config.ENVIRONMENT.useSentry) block() else null