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'
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/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/.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/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/build.gradle.kts b/build.gradle.kts
index f767d28..682713c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -14,11 +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/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
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..1f00ba1
--- /dev/null
+++ b/src/main/kotlin/dev/schlaubi/votebot/Launcher.kt
@@ -0,0 +1,57 @@
+/*
+ * 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 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.useSentry) {
+ { it.dsn = Config.SENTRY_TOKEN; }
+ } else {
+ { it.dsn = "" }
+ }
+
+ Sentry.init(configure)
+}
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..1963a86
--- /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)
+}
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..a4a3e28
--- /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
+ }
+ }
+}
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..000eb76
--- /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/errorhandling/DebugErrorHandler.kt b/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/DebugErrorHandler.kt
new file mode 100644
index 0000000..6c82d1a
--- /dev/null
+++ b/src/main/kotlin/dev/schlaubi/votebot/command/internal/errorhandling/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.errorhandling
+
+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/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/commands/ClaimPermissionsCommand.kt b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt
new file mode 100644
index 0000000..4aae022
--- /dev/null
+++ b/src/main/kotlin/dev/schlaubi/votebot/commands/ClaimPermissionsCommand.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.Embeds
+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(Embeds.error("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(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
new file mode 100644
index 0000000..434204e
--- /dev/null
+++ b/src/main/kotlin/dev/schlaubi/votebot/commands/InfoCommand.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.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"
+ override val name: String = "info"
+ override val useEphemeral: Boolean = true
+
+ override suspend fun execute(context: Context) {
+ 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
new file mode 100644
index 0000000..25f0a58
--- /dev/null
+++ b/src/main/kotlin/dev/schlaubi/votebot/commands/PermissionCommand.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.Embeds
+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(Embeds.error("Unknown command"))
+ return
+ }
+
+ appendPermission(
+ command.id,
+ context.bot.kord,
+ context.guild.id
+ ) {
+ addPermission(context.boolean("permission"))
+ }
+
+ context.respond(Embeds.success("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
new file mode 100644
index 0000000..fa2f13d
--- /dev/null
+++ b/src/main/kotlin/dev/schlaubi/votebot/config/Config.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.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.errorhandling.DebugErrorHandler
+import dev.schlaubi.votebot.command.internal.errorhandling.ProductionErrorHandler
+
+/**
+ * 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()
+}
+
+/**
+ * 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 = ProductionErrorHandler),
+
+ /**
+ * 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/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/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/util/EmbedUtils.kt b/src/main/kotlin/dev/schlaubi/votebot/util/EmbedUtils.kt
new file mode 100644
index 0000000..27891c9
--- /dev/null
+++ b/src/main/kotlin/dev/schlaubi/votebot/util/EmbedUtils.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.util
+
+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/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>"
+}
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/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt b/src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.kt
new file mode 100644
index 0000000..3b5a111
--- /dev/null
+++ b/src/main/kotlin/dev/schlaubi/votebot/util/SentryUtil.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.util
+
+import dev.schlaubi.votebot.config.Config
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+@OptIn(ExperimentalContracts::class)
+inline fun whenUsingSentry(block: () -> T): T? {
+ contract {
+ callsInPlace(block, InvocationKind.AT_MOST_ONCE)
+ }
+ return if (Config.ENVIRONMENT.useSentry)
+ block()
+ else null
+}
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