From e34889ac3e4e15f8ab68448c9c9022a4a8d14d6d Mon Sep 17 00:00:00 2001 From: DarkAtra Date: Sun, 5 Jun 2022 23:21:49 +0200 Subject: [PATCH] feat: move to the spring ecosystem * use semantic release * provide a docker image (using github actions) * add .editorconfig --- .editorconfig | 22 ++ .github/dependabot.yml | 5 + .github/workflows/build.yml | 69 ++++ .github/workflows/pull-request-lint.yml | 20 ++ .github/workflows/pull-request.yml | 33 ++ .gitignore | 15 +- .releaserc.json | 28 ++ pom.xml | 336 +++++++++--------- .../kotlin/de/darkatra/vrising/Disposable.kt | 6 - .../kotlin/de/darkatra/vrising/discord/Bot.kt | 76 ++-- .../darkatra/vrising/discord/BotProperties.kt | 26 ++ .../vrising/discord/ServerQueryClient.kt | 49 +++ .../vrising/discord/ServerStatusEmbed.kt | 98 ++--- .../vrising/discord/ServerStatusMonitor.kt | 17 +- .../discord/ServerStatusMonitorService.kt | 128 +++---- .../discord/command/AddServerCommand.kt | 96 ++--- .../vrising/discord/command/Command.kt | 12 +- .../discord/command/ListServersCommand.kt | 46 +-- .../discord/command/RemoveServerCommand.kt | 54 +-- .../vrising/serverquery/ServerQueryClient.kt | 47 --- src/main/resources/application.yml | 13 + 21 files changed, 708 insertions(+), 488 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/pull-request-lint.yml create mode 100644 .github/workflows/pull-request.yml create mode 100644 .releaserc.json delete mode 100644 src/main/kotlin/de/darkatra/vrising/Disposable.kt create mode 100644 src/main/kotlin/de/darkatra/vrising/discord/BotProperties.kt create mode 100644 src/main/kotlin/de/darkatra/vrising/discord/ServerQueryClient.kt delete mode 100644 src/main/kotlin/de/darkatra/vrising/serverquery/ServerQueryClient.kt create mode 100644 src/main/resources/application.yml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9adef16 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 160 +tab_width = 4 + +[*.kt] +ij_continuation_indent_size = 4 +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_parameter_annotation_wrap = split_into_lines +ij_kotlin_variable_annotation_wrap = split_into_lines + +[{*.yaml, *.yml}] +indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index daec318..8332ef3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,8 @@ updates: directory: "/" schedule: interval: "daily" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..014bdbf --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,69 @@ +name: Build +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'src/**' + - 'pom.xml' + +permissions: + # used by semantic release + contents: write + issues: write + pull-requests: write + # used to publish the docker image + packages: write + # used by trivy + security-events: write + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-maven- + + - name: Run Tests + run: mvn -B -ntp verify + + - name: Create new release + uses: cycjimmy/semantic-release-action@v3 + with: + extra_plugins: | + @semantic-release/git + @semantic-release/exec + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get Image Name + id: get-image-name + run: echo "::set-output name=image-name::$(mvn help:evaluate -Dexpression=image.name -q -DforceStdout)" + + - name: Scan Docker Image for Vulnerabilities + uses: aquasecurity/trivy-action@0.3.0 + with: + image-ref: ${{ steps.get-image-name.outputs.image-name }} + format: sarif + output: trivy-results.sarif + + - name: Upload Trivy Results to GitHub Security Tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: trivy-results.sarif diff --git a/.github/workflows/pull-request-lint.yml b/.github/workflows/pull-request-lint.yml new file mode 100644 index 0000000..81af90f --- /dev/null +++ b/.github/workflows/pull-request-lint.yml @@ -0,0 +1,20 @@ +name: Lint Pull Request +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + pull-requests: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + validateSingleCommit: true diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..f9a00a5 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,33 @@ +name: Verify Pull Request +on: + pull_request: + paths: + - 'src/**' + - 'pom.xml' + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-maven- + + - name: Run tests + run: mvn -B -ntp verify diff --git a/.gitignore b/.gitignore index 65f20b1..f609a09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,10 @@ +HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ -### IntelliJ IDEA ### -.idea/ -*.iws -*.iml -*.ipr - -### Eclipse ### +### STS ### .apt_generated .classpath .factorypath @@ -18,6 +13,12 @@ target/ .springBeans .sts4-cache +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + ### NetBeans ### /nbproject/private/ /nbbuild/ diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..9efacd4 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,28 @@ +{ + "branches": [ + "main" + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/exec", + { + "prepareCmd": "mvn -B -ntp versions:set -DgenerateBackupPoms=false -DnewVersion=${nextRelease.version}", + "publishCmd": "mvn -B -ntp -Ppublish-ghcr -DskipTests spring-boot:build-image" + } + ], + [ + "@semantic-release/git", + { + "assets": [ + [ + "**/pom.xml", + "!**/target/**/*" + ] + ] + } + ], + "@semantic-release/github" + ] +} diff --git a/pom.xml b/pom.xml index eed748d..37012be 100644 --- a/pom.xml +++ b/pom.xml @@ -1,175 +1,165 @@ - - - 4.0.0 - - de.darkatra - v-rising-discord-bot - 0.1.0 - jar - - v-rising-discord-bot - - - UTF-8 - UTF-8 - ${java.version} - ${java.version} - - de.darkatra.vrising.discord.BotKt - - 11 - 1.6.21 - official - - 0.8.0-M14 - 1.0.0 - 4.0.1 - 2.0.2 - 1.3.3 - 0.9.0-beta - - - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - - - dev.kord - kord-core - ${kord.version} - - - - - com.ibasco.agql - agql-source-query - ${agql-source-query.version} - - - - - com.fasterxml.uuid - java-uuid-generator - ${java-uuid-generator.version} - - - - - io.ktor - ktor-serialization-kotlinx-json-jvm - ${ktor.version} - - - - org.jetbrains.kotlinx - kotlinx-serialization-core-jvm - ${kotlinx-serialization-core-jvm.version} - - - - - org.kodein.db - kodein-db-jvm - ${kodein.version} - - - org.kodein.db - kodein-db-serializer-kotlinx-jvm - ${kodein.version} - - - org.kodein.db - kodein-leveldb-jni-jvm-windows - ${kodein.version} - - - - org.jetbrains.kotlin - kotlin-test-junit - ${kotlin.version} - test - - - org.jetbrains.kotlin - kotlin-test-junit5 - ${kotlin.version} - test - - - - - src/main/kotlin - src/test/kotlin - - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - compile - compile - - compile - - - - test-compile - test-compile - - test-compile - - - - - ${java.version} - - kotlinx-serialization - - - -Xjsr305=strict - -Xemit-jvm-type-annotations - -opt-in=kotlin.RequiresOptIn - - - - - org.jetbrains.kotlin - kotlin-maven-serialization - ${kotlin.version} - - - - - org.apache.maven.plugins - maven-assembly-plugin - 2.6 - - - make-assembly - package - - single - - - - - ${main.class} - - - - jar-with-dependencies - - - - - - - + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.0 + + + + de.darkatra + v-rising-discord-bot + 1.0.0 + jar + + + + MIT + https://opensource.org/licenses/MIT + repo + + + + + scm:git:git@github.com:DarkAtra/v-rising-discord-bot.git + https://github.com/DarkAtra/v-rising-discord-bot + + + + ghcr.io/darkatra/${project.artifactId}:${project.version} + + 11 + 1.6.21 + official + + 0.8.0-M14 + 1.0.0 + 4.0.1 + 3.4.4 + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-validation + + + + + dev.kord + kord-core + ${kord.version} + + + + + com.ibasco.agql + agql-source-query + ${agql-source-query.version} + + + + + com.fasterxml.uuid + java-uuid-generator + ${java-uuid-generator.version} + + + + + org.dizitart + potassium-nitrite + ${potassium-nitrite.version} + + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test + + + + + src/main/kotlin + src/test/kotlin + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + spring + + + -Xjsr305=strict + -opt-in=kotlin.RequiresOptIn + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + org.springframework.boot + spring-boot-maven-plugin + + + ${image.name} + paketobuildpacks/builder:tiny + + ${project.scm.url} + + + + + + + + + + publish-ghcr + + + + org.springframework.boot + spring-boot-maven-plugin + + + + true + + + + https://ghcr.io + anonymous + ${env.GITHUB_TOKEN} + + + + + + + + diff --git a/src/main/kotlin/de/darkatra/vrising/Disposable.kt b/src/main/kotlin/de/darkatra/vrising/Disposable.kt deleted file mode 100644 index 9a4237c..0000000 --- a/src/main/kotlin/de/darkatra/vrising/Disposable.kt +++ /dev/null @@ -1,6 +0,0 @@ -package de.darkatra.vrising - -interface Disposable { - - fun destroy() -} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/Bot.kt b/src/main/kotlin/de/darkatra/vrising/discord/Bot.kt index 86a2376..da0b624 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/Bot.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/Bot.kt @@ -1,47 +1,47 @@ package de.darkatra.vrising.discord -import de.darkatra.vrising.discord.command.AddServerCommand import de.darkatra.vrising.discord.command.Command -import de.darkatra.vrising.discord.command.ListServersCommand -import de.darkatra.vrising.discord.command.RemoveServerCommand -import de.darkatra.vrising.serverquery.ServerQueryClient import dev.kord.core.Kord import dev.kord.core.event.gateway.ReadyEvent import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent import dev.kord.core.on - -suspend fun main(args: Array) { - - if (args.size != 1) { - println("Expected exactly one argument containing the discord bot token.") - return - } - - val kord = Kord( - token = args[0] - ) { - enableShutdownHook = true - } - - val commands: List = listOf( - AddServerCommand(), - ListServersCommand(), - RemoveServerCommand() - ) - - kord.on { - val command = commands.find { command -> command.isSupported(interaction) } ?: return@on - command.handle(interaction) - } - - kord.on { - commands.forEach { command -> command.register(kord) } - ServerStatusMonitorService.launchServerStatusMonitor(kord) - } - - kord.login() - - ServerStatusMonitorService.destroy() - ServerQueryClient.destroy() +import kotlinx.coroutines.runBlocking +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.runApplication + +@SpringBootApplication +@EnableConfigurationProperties(BotProperties::class) +class Bot( + private val botProperties: BotProperties, + private val commands: List, + private val serverStatusMonitorService: ServerStatusMonitorService +) : ApplicationRunner { + + override fun run(args: ApplicationArguments) = runBlocking { + + val kord = Kord( + token = botProperties.discordBotToken + ) { + enableShutdownHook = true + } + + kord.on { + val command = commands.find { command -> command.isSupported(interaction) } ?: return@on + command.handle(interaction) + } + + kord.on { + commands.forEach { command -> command.register(kord) } + serverStatusMonitorService.launchServerStatusMonitor(kord) + } + + kord.login() + } } +fun main(args: Array) { + runApplication(*args) +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/BotProperties.kt b/src/main/kotlin/de/darkatra/vrising/discord/BotProperties.kt new file mode 100644 index 0000000..a4cfba0 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/BotProperties.kt @@ -0,0 +1,26 @@ +package de.darkatra.vrising.discord + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.ConstructorBinding +import org.springframework.validation.annotation.Validated +import java.nio.file.Path +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull + +@Validated +@ConstructorBinding +@ConfigurationProperties("bot") +class BotProperties { + + @field:NotBlank + lateinit var discordBotToken: String + + @field:NotNull + lateinit var databasePath: Path + + @field:NotBlank + lateinit var databaseUsername: String + + @field:NotBlank + lateinit var databasePassword: String +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/ServerQueryClient.kt b/src/main/kotlin/de/darkatra/vrising/discord/ServerQueryClient.kt new file mode 100644 index 0000000..e9a63e5 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/ServerQueryClient.kt @@ -0,0 +1,49 @@ +package de.darkatra.vrising.discord + +import com.ibasco.agql.core.enums.RateLimitType +import com.ibasco.agql.core.util.FailsafeOptions +import com.ibasco.agql.core.util.GeneralOptions +import com.ibasco.agql.protocols.valve.source.query.SourceQueryClient +import com.ibasco.agql.protocols.valve.source.query.SourceQueryOptions +import com.ibasco.agql.protocols.valve.source.query.info.SourceServer +import com.ibasco.agql.protocols.valve.source.query.players.SourcePlayer +import org.springframework.beans.factory.DisposableBean +import org.springframework.stereotype.Service +import java.net.InetSocketAddress +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@Service +class ServerQueryClient : DisposableBean { + + private val executor: ExecutorService = Executors.newCachedThreadPool() + private val queryOptions = SourceQueryOptions.builder() + .option(FailsafeOptions.FAILSAFE_RATELIMIT_TYPE, RateLimitType.BURST) + .option(GeneralOptions.THREAD_EXECUTOR_SERVICE, executor) + .build() + + fun getServerInfo(serverHostName: String, serverQueryPort: Int): SourceServer { + val address = InetSocketAddress(serverHostName, serverQueryPort) + return SourceQueryClient(queryOptions).use { client -> + client.getInfo(address).join().result + } + } + + fun getPlayerList(serverHostName: String, serverQueryPort: Int): List { + val address = InetSocketAddress(serverHostName, serverQueryPort) + return SourceQueryClient(queryOptions).use { client -> + client.getPlayers(address).join().result + }.filter { player -> player.name.isNotBlank() } + } + + fun getRules(serverHostName: String, serverQueryPort: Int): Map { + val address = InetSocketAddress(serverHostName, serverQueryPort) + return SourceQueryClient(queryOptions).use { client -> + client.getRules(address).join().result + } + } + + override fun destroy() { + executor.shutdown() + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusEmbed.kt b/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusEmbed.kt index 281e6d8..a8ee2f4 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusEmbed.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusEmbed.kt @@ -13,60 +13,60 @@ import dev.kord.rest.builder.message.modify.embed object ServerStatusEmbed { - suspend fun create(serverInfo: SourceServer, players: List, rules: Map, channel: MessageChannelBehavior): Snowflake { - return channel.createEmbed { - buildEmbed(serverInfo, players, rules, this) - }.id - } + suspend fun create(serverInfo: SourceServer, players: List, rules: Map, channel: MessageChannelBehavior): Snowflake { + return channel.createEmbed { + buildEmbed(serverInfo, players, rules, this) + }.id + } - suspend fun update(serverInfo: SourceServer, players: List, rules: Map, message: Message): Snowflake { - return message.edit { - embed { - buildEmbed(serverInfo, players, rules, this) - } - }.id - } + suspend fun update(serverInfo: SourceServer, players: List, rules: Map, message: Message): Snowflake { + return message.edit { + embed { + buildEmbed(serverInfo, players, rules, this) + } + }.id + } - private fun buildEmbed(serverInfo: SourceServer, players: List, rules: Map, embedBuilder: EmbedBuilder) { - embedBuilder.apply { - title = "Server Status" - color = Color( - red = 0, - green = 142, - blue = 68 - ) + private fun buildEmbed(serverInfo: SourceServer, players: List, rules: Map, embedBuilder: EmbedBuilder) { + embedBuilder.apply { + title = "Server Status" + color = Color( + red = 0, + green = 142, + blue = 68 + ) - field { - name = "Server name" - value = serverInfo.name - inline = false - } + field { + name = "Server name" + value = serverInfo.name + inline = false + } - field { - name = "Ip and Port" - value = "${serverInfo.hostAddress}:${serverInfo.port}" - inline = true - } + field { + name = "Ip and Port" + value = "${serverInfo.hostAddress}:${serverInfo.port}" + inline = true + } - field { - name = "Online count" - value = "${serverInfo.numOfPlayers}" - inline = true - } + field { + name = "Online count" + value = "${serverInfo.numOfPlayers}" + inline = true + } - rules["days-running"]?.let { currentDay -> - field { - name = "Ingame days" - value = "$currentDay" - inline = true - } - } + rules["days-running"]?.let { currentDay -> + field { + name = "Ingame days" + value = "$currentDay" + inline = true + } + } - field { - name = "Online players" - value = players.sortedBy { player -> player.name }.joinToString(separator = "\n") { player -> "**${player.name}** - ${player.score}" } - inline = false - } - } - } + field { + name = "Online players" + value = players.sortedBy { player -> player.name }.joinToString(separator = "\n") { player -> "**${player.name}** - ${player.score}" } + inline = false + } + } + } } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusMonitor.kt b/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusMonitor.kt index 2fd61d9..b15086b 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusMonitor.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusMonitor.kt @@ -1,15 +1,12 @@ package de.darkatra.vrising.discord -import dev.kord.common.entity.Snowflake -import kotlinx.serialization.Serializable -import org.kodein.db.model.Id +import org.dizitart.no2.objects.Id -@Serializable data class ServerStatusMonitor( - @Id - val id: String, - val hostName: String, - val queryPort: Int, - val discordChannelId: Snowflake, - var currentEmbedMessageId: Snowflake? = null + @Id + val id: String, + val hostName: String, + val queryPort: Int, + val discordChannelId: String, + var currentEmbedMessageId: String? = null ) diff --git a/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusMonitorService.kt b/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusMonitorService.kt index 034d95f..e8e2a64 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusMonitorService.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/ServerStatusMonitorService.kt @@ -1,86 +1,94 @@ package de.darkatra.vrising.discord -import de.darkatra.vrising.Disposable -import de.darkatra.vrising.serverquery.ServerQueryClient +import dev.kord.common.entity.Snowflake import dev.kord.core.Kord import dev.kord.core.behavior.channel.MessageChannelBehavior import dev.kord.core.exception.EntityNotFoundException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import org.kodein.db.DB -import org.kodein.db.asModelSequence -import org.kodein.db.deleteById -import org.kodein.db.impl.open -import org.kodein.db.orm.kotlinx.KotlinxSerializer +import org.dizitart.no2.Nitrite +import org.dizitart.no2.objects.filters.ObjectFilters +import org.springframework.beans.factory.DisposableBean +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.stereotype.Service import java.time.Duration import kotlin.coroutines.CoroutineContext +import kotlin.io.path.absolutePathString -object ServerStatusMonitorService : CoroutineScope, Disposable { +@Service +@EnableConfigurationProperties(BotProperties::class) +class ServerStatusMonitorService( + private val serverQueryClient: ServerQueryClient, + botProperties: BotProperties +) : CoroutineScope, DisposableBean { - override val coroutineContext: CoroutineContext = Dispatchers.Default + override val coroutineContext: CoroutineContext = Dispatchers.Default - private val database = DB.open("./db", KotlinxSerializer { - ServerStatusMonitor.serializer() - }) + private var database = Nitrite.builder() + .compressed() + .filePath(botProperties.databasePath.absolutePathString()) + .openOrCreate(botProperties.databaseUsername, botProperties.databasePassword) - fun putServerStatusMonitor(serverStatusMonitor: ServerStatusMonitor) { - database.put(serverStatusMonitor) - } + private var repository = database.getRepository(ServerStatusMonitor::class.java) - fun removeServerStatusMonitor(id: String) { - database.deleteById(id) - } + fun putServerStatusMonitor(serverStatusMonitor: ServerStatusMonitor) { + if (repository.find(ObjectFilters.eq("id", serverStatusMonitor.id)).firstOrNull() != null) { + repository.update(serverStatusMonitor) + } else { + repository.insert(serverStatusMonitor) + } + } - fun getServerStatusMonitors(): List { - return database.find(ServerStatusMonitor::class).all().use { cursor -> - cursor.asModelSequence().toList() - } - } + fun removeServerStatusMonitor(id: String) { + repository.remove(ObjectFilters.eq("id", id)) + } - fun launchServerStatusMonitor(kord: Kord) { - launch { - while (isActive) { - runCatching { - getServerStatusMonitors().forEach { serverStatusConfiguration -> + fun getServerStatusMonitors(): List { + return repository.find().toList() + } - val channel = kord.getChannel(serverStatusConfiguration.discordChannelId) - if (channel == null || channel !is MessageChannelBehavior) { - return@forEach - } + fun launchServerStatusMonitor(kord: Kord) { + launch { + while (isActive) { + runCatching { + getServerStatusMonitors().forEach { serverStatusConfiguration -> - val serverInfo = ServerQueryClient.getServerInfo(serverStatusConfiguration.hostName, serverStatusConfiguration.queryPort) - val players = ServerQueryClient.getPlayerList(serverStatusConfiguration.hostName, serverStatusConfiguration.queryPort) - val rules = ServerQueryClient.getRules(serverStatusConfiguration.hostName, serverStatusConfiguration.queryPort) + val channel = kord.getChannel(Snowflake(serverStatusConfiguration.discordChannelId)) + if (channel == null || channel !is MessageChannelBehavior) { + return@forEach + } - val currentEmbedMessageId = serverStatusConfiguration.currentEmbedMessageId - if (currentEmbedMessageId != null) { - try { - ServerStatusEmbed.update(serverInfo, players, rules, channel.getMessage(currentEmbedMessageId)) - return@forEach - } catch (e: EntityNotFoundException) { - serverStatusConfiguration.currentEmbedMessageId = null - } - } + val serverInfo = serverQueryClient.getServerInfo(serverStatusConfiguration.hostName, serverStatusConfiguration.queryPort) + val players = serverQueryClient.getPlayerList(serverStatusConfiguration.hostName, serverStatusConfiguration.queryPort) + val rules = serverQueryClient.getRules(serverStatusConfiguration.hostName, serverStatusConfiguration.queryPort) - serverStatusConfiguration.currentEmbedMessageId = ServerStatusEmbed.create(serverInfo, players, rules, channel) - putServerStatusMonitor(serverStatusConfiguration) - } + val currentEmbedMessageId = serverStatusConfiguration.currentEmbedMessageId + if (currentEmbedMessageId != null) { + try { + ServerStatusEmbed.update(serverInfo, players, rules, channel.getMessage(Snowflake(currentEmbedMessageId))) + return@forEach + } catch (e: EntityNotFoundException) { + serverStatusConfiguration.currentEmbedMessageId = null + } + } - delay(Duration.ofMinutes(1).toMillis()) - }.onFailure { throwable -> - println("Exception in status monitoring thread: ${throwable.message}") - throwable.printStackTrace() - } - } - } - } + serverStatusConfiguration.currentEmbedMessageId = ServerStatusEmbed.create(serverInfo, players, rules, channel).toString() + putServerStatusMonitor(serverStatusConfiguration) + } + }.onFailure { throwable -> + println("Exception in status monitoring thread: ${throwable.message}") + throwable.printStackTrace() + } - override fun destroy() { - cancel() - database.close() - } + delay(Duration.ofMinutes(1).toMillis()) + } + } + } + + override fun destroy() { + database.close() + } } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/command/AddServerCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/command/AddServerCommand.kt index 0cf35e3..0239eaf 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/command/AddServerCommand.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/command/AddServerCommand.kt @@ -8,54 +8,58 @@ import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.entity.interaction.ChatInputCommandInteraction import dev.kord.rest.builder.interaction.int import dev.kord.rest.builder.interaction.string +import org.springframework.stereotype.Component +@Component class AddServerCommand( - private val name: String = "add-server", - private val description: String = "Adds a server to the status monitor." + private val serverStatusMonitorService: ServerStatusMonitorService, ) : Command { - override fun getCommandName(): String = name - - override suspend fun register(kord: Kord) { - - kord.createGlobalChatInputCommand( - name = name, - description = description - ) { - - string( - name = "server-hostname", - description = "The hostname of the server to add a status monitor for." - ) { - required = true - } - - int( - name = "server-query-port", - description = "The query port of the server to add a status monitor for." - ) { - required = true - } - } - } - - override suspend fun handle(interaction: ChatInputCommandInteraction) { - - val command = interaction.command - val hostName = command.strings["server-hostname"]!! - val queryPort = Math.toIntExact(command.integers["server-query-port"]!!) - val channelId = interaction.channelId - - ServerStatusMonitorService.putServerStatusMonitor(ServerStatusMonitor( - id = Generators.timeBasedGenerator().generate().toString(), - hostName = hostName, - queryPort = queryPort, - discordChannelId = channelId - )) - - val response = interaction.deferEphemeralResponse() - response.respond { - content = "Added monitor for '${hostName}:${queryPort}' to channel '$channelId'." - } - } + private val name: String = "add-server" + private val description: String = "Adds a server to the status monitor." + + override fun getCommandName(): String = name + + override suspend fun register(kord: Kord) { + + kord.createGlobalChatInputCommand( + name = name, + description = description + ) { + + string( + name = "server-hostname", + description = "The hostname of the server to add a status monitor for." + ) { + required = true + } + + int( + name = "server-query-port", + description = "The query port of the server to add a status monitor for." + ) { + required = true + } + } + } + + override suspend fun handle(interaction: ChatInputCommandInteraction) { + + val command = interaction.command + val hostName = command.strings["server-hostname"]!! + val queryPort = Math.toIntExact(command.integers["server-query-port"]!!) + val channelId = interaction.channelId + + serverStatusMonitorService.putServerStatusMonitor(ServerStatusMonitor( + id = Generators.timeBasedGenerator().generate().toString(), + hostName = hostName, + queryPort = queryPort, + discordChannelId = channelId.toString() + )) + + val response = interaction.deferEphemeralResponse() + response.respond { + content = "Added monitor for '${hostName}:${queryPort}' to channel '$channelId'." + } + } } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/command/Command.kt b/src/main/kotlin/de/darkatra/vrising/discord/command/Command.kt index 2c6d63e..7c25dfb 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/command/Command.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/command/Command.kt @@ -5,13 +5,13 @@ import dev.kord.core.entity.interaction.ChatInputCommandInteraction interface Command { - fun getCommandName(): String + fun getCommandName(): String - suspend fun register(kord: Kord) + suspend fun register(kord: Kord) - fun isSupported(interaction: ChatInputCommandInteraction): Boolean { - return interaction.invokedCommandName == getCommandName() && !interaction.user.isBot - } + fun isSupported(interaction: ChatInputCommandInteraction): Boolean { + return interaction.invokedCommandName == getCommandName() && !interaction.user.isBot + } - suspend fun handle(interaction: ChatInputCommandInteraction) + suspend fun handle(interaction: ChatInputCommandInteraction) } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/command/ListServersCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/command/ListServersCommand.kt index 7355d60..da3bb0e 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/command/ListServersCommand.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/command/ListServersCommand.kt @@ -4,34 +4,38 @@ import de.darkatra.vrising.discord.ServerStatusMonitorService import dev.kord.core.Kord import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.entity.interaction.ChatInputCommandInteraction +import org.springframework.stereotype.Component +@Component class ListServersCommand( - private val name: String = "list-servers", - private val description: String = "Lists all server status monitors." + private val serverStatusMonitorService: ServerStatusMonitorService, ) : Command { - override fun getCommandName(): String = name + private val name: String = "list-servers" + private val description: String = "Lists all server status monitors." - override suspend fun register(kord: Kord) { + override fun getCommandName(): String = name - kord.createGlobalChatInputCommand( - name = name, - description = description - ) - } + override suspend fun register(kord: Kord) { - override suspend fun handle(interaction: ChatInputCommandInteraction) { + kord.createGlobalChatInputCommand( + name = name, + description = description + ) + } - val serverStatusConfigurations = ServerStatusMonitorService.getServerStatusMonitors() + override suspend fun handle(interaction: ChatInputCommandInteraction) { - val response = interaction.deferEphemeralResponse() - response.respond { - content = when (serverStatusConfigurations.isEmpty()) { - true -> "No servers found." - false -> serverStatusConfigurations.joinToString(separator = "\n") { serverStatusConfiguration -> - "${serverStatusConfiguration.id} - ${serverStatusConfiguration.hostName}:${serverStatusConfiguration.queryPort}" - } - } - } - } + val serverStatusConfigurations = serverStatusMonitorService.getServerStatusMonitors() + + val response = interaction.deferEphemeralResponse() + response.respond { + content = when (serverStatusConfigurations.isEmpty()) { + true -> "No servers found." + false -> serverStatusConfigurations.joinToString(separator = "\n") { serverStatusConfiguration -> + "${serverStatusConfiguration.id} - ${serverStatusConfiguration.hostName}:${serverStatusConfiguration.queryPort}" + } + } + } + } } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/command/RemoveServerCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/command/RemoveServerCommand.kt index 5a1c60f..5da112a 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/command/RemoveServerCommand.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/command/RemoveServerCommand.kt @@ -5,40 +5,44 @@ import dev.kord.core.Kord import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.entity.interaction.ChatInputCommandInteraction import dev.kord.rest.builder.interaction.string +import org.springframework.stereotype.Component +@Component class RemoveServerCommand( - private val name: String = "remove-server", - private val description: String = "Removes a server from the status monitor." + private val serverStatusMonitorService: ServerStatusMonitorService, ) : Command { - override fun getCommandName(): String = name + private val name: String = "remove-server" + private val description: String = "Removes a server from the status monitor." - override suspend fun register(kord: Kord) { + override fun getCommandName(): String = name - kord.createGlobalChatInputCommand( - name = name, - description = description - ) { + override suspend fun register(kord: Kord) { - string( - name = "server-status-monitor-id", - description = "The id of the server status monitor." - ) { - required = true - } - } - } + kord.createGlobalChatInputCommand( + name = name, + description = description + ) { - override suspend fun handle(interaction: ChatInputCommandInteraction) { + string( + name = "server-status-monitor-id", + description = "The id of the server status monitor." + ) { + required = true + } + } + } - val command = interaction.command - val serverStatusMonitorId = command.strings["server-status-monitor-id"]!! + override suspend fun handle(interaction: ChatInputCommandInteraction) { - ServerStatusMonitorService.removeServerStatusMonitor(serverStatusMonitorId) + val command = interaction.command + val serverStatusMonitorId = command.strings["server-status-monitor-id"]!! - val response = interaction.deferEphemeralResponse() - response.respond { - content = "Removed monitor with id '$serverStatusMonitorId'." - } - } + serverStatusMonitorService.removeServerStatusMonitor(serverStatusMonitorId) + + val response = interaction.deferEphemeralResponse() + response.respond { + content = "Removed monitor with id '$serverStatusMonitorId'." + } + } } diff --git a/src/main/kotlin/de/darkatra/vrising/serverquery/ServerQueryClient.kt b/src/main/kotlin/de/darkatra/vrising/serverquery/ServerQueryClient.kt deleted file mode 100644 index 239a4ec..0000000 --- a/src/main/kotlin/de/darkatra/vrising/serverquery/ServerQueryClient.kt +++ /dev/null @@ -1,47 +0,0 @@ -package de.darkatra.vrising.serverquery - -import com.ibasco.agql.core.enums.RateLimitType -import com.ibasco.agql.core.util.FailsafeOptions -import com.ibasco.agql.core.util.GeneralOptions -import com.ibasco.agql.protocols.valve.source.query.SourceQueryClient -import com.ibasco.agql.protocols.valve.source.query.SourceQueryOptions -import com.ibasco.agql.protocols.valve.source.query.info.SourceServer -import com.ibasco.agql.protocols.valve.source.query.players.SourcePlayer -import de.darkatra.vrising.Disposable -import java.net.InetSocketAddress -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors - -object ServerQueryClient : Disposable { - - private val executor: ExecutorService = Executors.newCachedThreadPool() - private val queryOptions = SourceQueryOptions.builder() - .option(FailsafeOptions.FAILSAFE_RATELIMIT_TYPE, RateLimitType.BURST) - .option(GeneralOptions.THREAD_EXECUTOR_SERVICE, executor) - .build() - - fun getServerInfo(serverHostName: String, serverQueryPort: Int): SourceServer { - val address = InetSocketAddress(serverHostName, serverQueryPort) - return SourceQueryClient(queryOptions).use { client -> - client.getInfo(address).join().result - } - } - - fun getPlayerList(serverHostName: String, serverQueryPort: Int): List { - val address = InetSocketAddress(serverHostName, serverQueryPort) - return SourceQueryClient(queryOptions).use { client -> - client.getPlayers(address).join().result - }.filter { player -> player.name.isNotBlank() } - } - - fun getRules(serverHostName: String, serverQueryPort: Int): Map { - val address = InetSocketAddress(serverHostName, serverQueryPort) - return SourceQueryClient(queryOptions).use { client -> - client.getRules(address).join().result - } - } - - override fun destroy() { - executor.shutdown() - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..6f03f95 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,13 @@ +spring: + main: + web-application-type: none + +logging: + level: + root: info + +bot: + discord-bot-token: ~ + database-path: ./bot.db + database-username: v-rising-discord-bot + database-password: ~