diff --git a/pom.xml b/pom.xml index 39b6bca..ebc9fe5 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ de.darkatra v-rising-discord-bot - 2.10.5 + 2.11.0 jar diff --git a/src/main/kotlin/de/darkatra/vrising/discord/Bot.kt b/src/main/kotlin/de/darkatra/vrising/discord/Bot.kt index 7ea5846..842059c 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/Bot.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/Bot.kt @@ -8,8 +8,13 @@ import de.darkatra.vrising.discord.commands.Command import de.darkatra.vrising.discord.migration.DatabaseMigrationService import de.darkatra.vrising.discord.migration.Schema import de.darkatra.vrising.discord.persistence.model.Error -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor -import de.darkatra.vrising.discord.serverstatus.ServerStatusMonitorService +import de.darkatra.vrising.discord.persistence.model.Leaderboard +import de.darkatra.vrising.discord.persistence.model.PlayerActivityFeed +import de.darkatra.vrising.discord.persistence.model.PvpKillFeed +import de.darkatra.vrising.discord.persistence.model.Server +import de.darkatra.vrising.discord.persistence.model.Status +import de.darkatra.vrising.discord.persistence.model.StatusMonitor +import de.darkatra.vrising.discord.serverstatus.ServerService import dev.kord.core.Kord import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.event.gateway.ReadyEvent @@ -43,23 +48,31 @@ import java.util.concurrent.atomic.AtomicBoolean @ImportRuntimeHints(BotRuntimeHints::class) @EnableConfigurationProperties(BotProperties::class) @RegisterReflectionForBinding( + // properties BotProperties::class, - Schema::class, - ServerStatusMonitor::class, + // database Error::class, + Leaderboard::class, + Schema::class, + PlayerActivityFeed::class, + PvpKillFeed::class, + Server::class, + Status::class, + StatusMonitor::class, + // http Character::class, - VBlood::class, PlayerActivity::class, PlayerActivity.Type::class, PvpKill::class, - PvpKill.Player::class + PvpKill.Player::class, + VBlood::class, ) class Bot( private val database: Nitrite, private val botProperties: BotProperties, private val commands: List, private val databaseMigrationService: DatabaseMigrationService, - private val serverStatusMonitorService: ServerStatusMonitorService + private val serverService: ServerService ) : ApplicationRunner, DisposableBean, SchedulingConfigurer { private val logger = LoggerFactory.getLogger(javaClass) @@ -80,8 +93,7 @@ class Bot( val command = commands.find { command -> command.isSupported(interaction, botProperties.adminUserIds) } if (command == null) { interaction.deferEphemeralResponse().respond { - content = """This command is not supported here, please refer to the documentation. - |Be sure to use the commands in the channel where you want the status message to appear.""".trimMargin() + content = "This command is not supported here, please refer to the documentation." } return@on } @@ -127,7 +139,7 @@ class Bot( { if (isReady.get() && kord.isActive) { runBlocking { - serverStatusMonitorService.updateServerStatusMonitors(kord) + serverService.updateServers(kord) } } }, @@ -142,7 +154,7 @@ class Bot( { if (isReady.get() && kord.isActive) { runBlocking { - serverStatusMonitorService.cleanupInactiveServerStatusMonitors(kord) + serverService.cleanupInactiveServers(kord) } } }, diff --git a/src/main/kotlin/de/darkatra/vrising/discord/KordExtensions.kt b/src/main/kotlin/de/darkatra/vrising/discord/KordExtensions.kt index 7d4d21c..fa8106f 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/KordExtensions.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/KordExtensions.kt @@ -1,11 +1,12 @@ package de.darkatra.vrising.discord -import de.darkatra.vrising.discord.serverstatus.exceptions.InvalidDiscordChannelException import dev.kord.common.entity.Snowflake import dev.kord.core.Kord import dev.kord.core.behavior.channel.MessageChannelBehavior import dev.kord.core.entity.interaction.InteractionCommand +private class InvalidDiscordChannelException(message: String, cause: Throwable? = null) : BotException(message, cause) + suspend fun Kord.getDiscordChannel(discordChannelId: String): Result { val channel = try { getChannel(Snowflake(discordChannelId)) @@ -25,3 +26,12 @@ fun InteractionCommand.getChannelIdFromStringParameter(parameterName: String): S val match = channelPattern.find(value) ?: return value return match.groups[1]!!.value } + +suspend fun MessageChannelBehavior.tryCreateMessage(message: String): Boolean { + try { + createMessage(message) + return true + } catch (e: Exception) { + return false + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/AddServerCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/AddServerCommand.kt index 6f42484..b30f0ad 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/AddServerCommand.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/AddServerCommand.kt @@ -4,31 +4,20 @@ import com.fasterxml.uuid.Generators import de.darkatra.vrising.discord.BotProperties import de.darkatra.vrising.discord.commands.parameters.ServerApiHostnameParameter import de.darkatra.vrising.discord.commands.parameters.ServerHostnameParameter -import de.darkatra.vrising.discord.commands.parameters.addDisplayPlayerGearLevelParameter -import de.darkatra.vrising.discord.commands.parameters.addDisplayServerDescriptionParameter -import de.darkatra.vrising.discord.commands.parameters.addEmbedEnabledParameter -import de.darkatra.vrising.discord.commands.parameters.addPlayerActivityFeedChannelIdParameter -import de.darkatra.vrising.discord.commands.parameters.addPvpKillFeedChannelIdParameter import de.darkatra.vrising.discord.commands.parameters.addServerApiHostnameParameter import de.darkatra.vrising.discord.commands.parameters.addServerApiPasswordParameter import de.darkatra.vrising.discord.commands.parameters.addServerApiPortParameter import de.darkatra.vrising.discord.commands.parameters.addServerApiUsernameParameter import de.darkatra.vrising.discord.commands.parameters.addServerHostnameParameter import de.darkatra.vrising.discord.commands.parameters.addServerQueryPortParameter -import de.darkatra.vrising.discord.commands.parameters.getDisplayPlayerGearLevelParameter -import de.darkatra.vrising.discord.commands.parameters.getDisplayServerDescriptionParameter -import de.darkatra.vrising.discord.commands.parameters.getEmbedEnabledParameter -import de.darkatra.vrising.discord.commands.parameters.getPlayerActivityFeedChannelIdParameter -import de.darkatra.vrising.discord.commands.parameters.getPvpKillFeedChannelIdParameter import de.darkatra.vrising.discord.commands.parameters.getServerApiHostnameParameter import de.darkatra.vrising.discord.commands.parameters.getServerApiPasswordParameter import de.darkatra.vrising.discord.commands.parameters.getServerApiPortParameter import de.darkatra.vrising.discord.commands.parameters.getServerApiUsernameParameter import de.darkatra.vrising.discord.commands.parameters.getServerHostnameParameter import de.darkatra.vrising.discord.commands.parameters.getServerQueryPortParameter -import de.darkatra.vrising.discord.persistence.ServerStatusMonitorRepository -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitorStatus +import de.darkatra.vrising.discord.persistence.ServerRepository +import de.darkatra.vrising.discord.persistence.model.Server import dev.kord.core.Kord import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.entity.interaction.ChatInputCommandInteraction @@ -41,14 +30,14 @@ import org.springframework.stereotype.Component @Component @EnableConfigurationProperties(BotProperties::class) class AddServerCommand( - private val serverStatusMonitorRepository: ServerStatusMonitorRepository, + private val serverRepository: ServerRepository, private val botProperties: BotProperties ) : Command { private val logger = LoggerFactory.getLogger(javaClass) private val name: String = "add-server" - private val description: String = "Adds a server to the status monitor." + private val description: String = "Adds a server." override fun getCommandName(): String = name @@ -69,13 +58,6 @@ class AddServerCommand( addServerApiPortParameter(required = false) addServerApiUsernameParameter(required = false) addServerApiPasswordParameter(required = false) - - addEmbedEnabledParameter(required = false) - addDisplayServerDescriptionParameter(required = false) - addDisplayPlayerGearLevelParameter(required = false) - - addPlayerActivityFeedChannelIdParameter(required = false) - addPvpKillFeedChannelIdParameter(required = false) } } @@ -96,45 +78,29 @@ class AddServerCommand( val apiUsername = interaction.getServerApiUsernameParameter() val apiPassword = interaction.getServerApiPasswordParameter() - val embedEnabled = interaction.getEmbedEnabledParameter() ?: true - val displayServerDescription = interaction.getDisplayServerDescriptionParameter() ?: true - val displayPlayerGearLevel = interaction.getDisplayPlayerGearLevelParameter() ?: true - - val playerActivityChannelId = interaction.getPlayerActivityFeedChannelIdParameter() - val pvpKillFeedChannelId = interaction.getPvpKillFeedChannelIdParameter() - val discordServerId = (interaction as GuildChatInputCommandInteraction).guildId - val channelId = interaction.channelId ServerHostnameParameter.validate(hostname, botProperties.allowLocalAddressRanges) ServerApiHostnameParameter.validate(apiHostname, botProperties.allowLocalAddressRanges) - val serverStatusMonitorId = Generators.timeBasedGenerator().generate() - serverStatusMonitorRepository.addServerStatusMonitor( - ServerStatusMonitor( - id = serverStatusMonitorId.toString(), + val serverId = Generators.timeBasedGenerator().generate() + serverRepository.addServer( + Server( + id = serverId.toString(), discordServerId = discordServerId.toString(), - discordChannelId = channelId.toString(), - playerActivityDiscordChannelId = playerActivityChannelId, - pvpKillFeedDiscordChannelId = pvpKillFeedChannelId, hostname = hostname, queryPort = queryPort, apiHostname = apiHostname, apiPort = apiPort, apiUsername = apiUsername, apiPassword = apiPassword, - status = ServerStatusMonitorStatus.ACTIVE, - embedEnabled = embedEnabled, - displayServerDescription = displayServerDescription, - displayPlayerGearLevel = displayPlayerGearLevel, ) ) - logger.info("Successfully added monitor with id '${serverStatusMonitorId}' for '${hostname}:${queryPort}' to channel '$channelId'.") + logger.info("Successfully added server '$serverId' for '$hostname:$queryPort' for discord server '$discordServerId'.") interaction.deferEphemeralResponse().respond { - content = """Added monitor with id '${serverStatusMonitorId}' for '${hostname}:${queryPort}' to channel '$channelId'. - |It may take a minute for the status message to appear.""".trimMargin() + content = "Added server with id '$serverId' for '$hostname:$queryPort' for discord server '$discordServerId'." } } } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/ConfigurePlayerActivityFeedCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/ConfigurePlayerActivityFeedCommand.kt new file mode 100644 index 0000000..a076bb7 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/ConfigurePlayerActivityFeedCommand.kt @@ -0,0 +1,114 @@ +package de.darkatra.vrising.discord.commands + +import de.darkatra.vrising.discord.commands.parameters.ChannelIdParameter +import de.darkatra.vrising.discord.commands.parameters.addChannelIdParameter +import de.darkatra.vrising.discord.commands.parameters.addServerIdParameter +import de.darkatra.vrising.discord.commands.parameters.addStatusParameter +import de.darkatra.vrising.discord.commands.parameters.getChannelIdParameter +import de.darkatra.vrising.discord.commands.parameters.getServerIdParameter +import de.darkatra.vrising.discord.commands.parameters.getStatusParameter +import de.darkatra.vrising.discord.persistence.ServerRepository +import de.darkatra.vrising.discord.persistence.model.PlayerActivityFeed +import de.darkatra.vrising.discord.persistence.model.Status +import dev.kord.core.Kord +import dev.kord.core.behavior.interaction.response.respond +import dev.kord.core.entity.interaction.ChatInputCommandInteraction +import dev.kord.core.entity.interaction.GlobalChatInputCommandInteraction +import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +class ConfigurePlayerActivityFeedCommand( + private val serverRepository: ServerRepository +) : Command { + + private val logger = LoggerFactory.getLogger(javaClass) + + private val name: String = "configure-player-activity-feed" + private val description: String = "Configures the player activity feed for a given server." + + override fun getCommandName(): String = name + + override suspend fun register(kord: Kord) { + + kord.createGlobalChatInputCommand( + name = name, + description = description + ) { + + dmPermission = false + disableCommandInGuilds() + + addServerIdParameter() + + addChannelIdParameter(required = false) + addStatusParameter(required = false) + } + } + + override fun isSupported(interaction: ChatInputCommandInteraction, adminUserIds: Set): Boolean { + if (interaction is GlobalChatInputCommandInteraction) { + return false + } + return super.isSupported(interaction, adminUserIds) + } + + override suspend fun handle(interaction: ChatInputCommandInteraction) { + + val serverId = interaction.getServerIdParameter() + + val channelId = interaction.getChannelIdParameter() + val status = interaction.getStatusParameter() + + val server = when (interaction) { + is GuildChatInputCommandInteraction -> serverRepository.getServer(serverId, interaction.guildId.toString()) + is GlobalChatInputCommandInteraction -> serverRepository.getServer(serverId) + } + + if (server == null) { + interaction.deferEphemeralResponse().respond { + content = "No server with id '$serverId' was found." + } + return + } + + val playerActivityFeed = server.playerActivityFeed + if (playerActivityFeed == null) { + + if (channelId == null) { + interaction.deferEphemeralResponse().respond { + content = "'${ChannelIdParameter.NAME}' is required when using this command for the first time." + } + return + } + + server.playerActivityFeed = PlayerActivityFeed( + status = status ?: Status.ACTIVE, + discordChannelId = channelId + ) + + logger.info("Successfully configured the player activity feed for server '$serverId'.") + + interaction.deferEphemeralResponse().respond { + content = "Successfully configured the player activity feed for server with id '$serverId'." + } + return + } + + if (channelId != null) { + playerActivityFeed.discordChannelId = channelId + } + if (status != null) { + playerActivityFeed.status = status + } + + serverRepository.updateServer(server) + + logger.info("Successfully updated the player activity feed for server '$serverId'.") + + interaction.deferEphemeralResponse().respond { + content = "Successfully updated the player activity feed for server with id '$serverId'." + } + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/ConfigurePvpKillFeedCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/ConfigurePvpKillFeedCommand.kt new file mode 100644 index 0000000..862c0f8 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/ConfigurePvpKillFeedCommand.kt @@ -0,0 +1,114 @@ +package de.darkatra.vrising.discord.commands + +import de.darkatra.vrising.discord.commands.parameters.ChannelIdParameter +import de.darkatra.vrising.discord.commands.parameters.addChannelIdParameter +import de.darkatra.vrising.discord.commands.parameters.addServerIdParameter +import de.darkatra.vrising.discord.commands.parameters.addStatusParameter +import de.darkatra.vrising.discord.commands.parameters.getChannelIdParameter +import de.darkatra.vrising.discord.commands.parameters.getServerIdParameter +import de.darkatra.vrising.discord.commands.parameters.getStatusParameter +import de.darkatra.vrising.discord.persistence.ServerRepository +import de.darkatra.vrising.discord.persistence.model.PvpKillFeed +import de.darkatra.vrising.discord.persistence.model.Status +import dev.kord.core.Kord +import dev.kord.core.behavior.interaction.response.respond +import dev.kord.core.entity.interaction.ChatInputCommandInteraction +import dev.kord.core.entity.interaction.GlobalChatInputCommandInteraction +import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +class ConfigurePvpKillFeedCommand( + private val serverRepository: ServerRepository +) : Command { + + private val logger = LoggerFactory.getLogger(javaClass) + + private val name: String = "configure-pvp-kill-feed" + private val description: String = "Configures the pvp kill feed for a given server." + + override fun getCommandName(): String = name + + override suspend fun register(kord: Kord) { + + kord.createGlobalChatInputCommand( + name = name, + description = description + ) { + + dmPermission = false + disableCommandInGuilds() + + addServerIdParameter() + + addChannelIdParameter(required = false) + addStatusParameter(required = false) + } + } + + override fun isSupported(interaction: ChatInputCommandInteraction, adminUserIds: Set): Boolean { + if (interaction is GlobalChatInputCommandInteraction) { + return false + } + return super.isSupported(interaction, adminUserIds) + } + + override suspend fun handle(interaction: ChatInputCommandInteraction) { + + val serverId = interaction.getServerIdParameter() + + val channelId = interaction.getChannelIdParameter() + val status = interaction.getStatusParameter() + + val server = when (interaction) { + is GuildChatInputCommandInteraction -> serverRepository.getServer(serverId, interaction.guildId.toString()) + is GlobalChatInputCommandInteraction -> serverRepository.getServer(serverId) + } + + if (server == null) { + interaction.deferEphemeralResponse().respond { + content = "No server with id '$serverId' was found." + } + return + } + + val pvpKillFeed = server.pvpKillFeed + if (pvpKillFeed == null) { + + if (channelId == null) { + interaction.deferEphemeralResponse().respond { + content = "'${ChannelIdParameter.NAME}' is required when using this command for the first time." + } + return + } + + server.pvpKillFeed = PvpKillFeed( + status = status ?: Status.ACTIVE, + discordChannelId = channelId + ) + + logger.info("Successfully configured the pvp kill feed for server '$serverId'.") + + interaction.deferEphemeralResponse().respond { + content = "Successfully configured the pvp kill feed for server with id '$serverId'." + } + return + } + + if (channelId != null) { + pvpKillFeed.discordChannelId = channelId + } + if (status != null) { + pvpKillFeed.status = status + } + + serverRepository.updateServer(server) + + logger.info("Successfully updated the pvp kill feed for server '$serverId'.") + + interaction.deferEphemeralResponse().respond { + content = "Successfully updated the pvp kill feed for server with id '$serverId'." + } + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/ConfigureStatusMonitorCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/ConfigureStatusMonitorCommand.kt new file mode 100644 index 0000000..c2079fe --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/ConfigureStatusMonitorCommand.kt @@ -0,0 +1,134 @@ +package de.darkatra.vrising.discord.commands + +import de.darkatra.vrising.discord.commands.parameters.ChannelIdParameter +import de.darkatra.vrising.discord.commands.parameters.addChannelIdParameter +import de.darkatra.vrising.discord.commands.parameters.addDisplayPlayerGearLevelParameter +import de.darkatra.vrising.discord.commands.parameters.addDisplayServerDescriptionParameter +import de.darkatra.vrising.discord.commands.parameters.addServerIdParameter +import de.darkatra.vrising.discord.commands.parameters.addStatusParameter +import de.darkatra.vrising.discord.commands.parameters.getChannelIdParameter +import de.darkatra.vrising.discord.commands.parameters.getDisplayPlayerGearLevelParameter +import de.darkatra.vrising.discord.commands.parameters.getDisplayServerDescriptionParameter +import de.darkatra.vrising.discord.commands.parameters.getServerIdParameter +import de.darkatra.vrising.discord.commands.parameters.getStatusParameter +import de.darkatra.vrising.discord.persistence.ServerRepository +import de.darkatra.vrising.discord.persistence.model.Status +import de.darkatra.vrising.discord.persistence.model.StatusMonitor +import dev.kord.core.Kord +import dev.kord.core.behavior.interaction.response.respond +import dev.kord.core.entity.interaction.ChatInputCommandInteraction +import dev.kord.core.entity.interaction.GlobalChatInputCommandInteraction +import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +@Component +class ConfigureStatusMonitorCommand( + private val serverRepository: ServerRepository +) : Command { + + private val logger = LoggerFactory.getLogger(javaClass) + + private val name: String = "configure-status-monitor" + private val description: String = "Configures the status monitor for a given server." + + override fun getCommandName(): String = name + + override suspend fun register(kord: Kord) { + + kord.createGlobalChatInputCommand( + name = name, + description = description + ) { + + dmPermission = false + disableCommandInGuilds() + + addServerIdParameter() + + addChannelIdParameter(required = false) + addStatusParameter(required = false) + + addDisplayServerDescriptionParameter(required = false) + addDisplayPlayerGearLevelParameter(required = false) + } + } + + override fun isSupported(interaction: ChatInputCommandInteraction, adminUserIds: Set): Boolean { + if (interaction is GlobalChatInputCommandInteraction) { + return false + } + return super.isSupported(interaction, adminUserIds) + } + + override suspend fun handle(interaction: ChatInputCommandInteraction) { + + val serverId = interaction.getServerIdParameter() + + val channelId = interaction.getChannelIdParameter() + val status = interaction.getStatusParameter() + + val displayServerDescription = interaction.getDisplayServerDescriptionParameter() + val displayPlayerGearLevel = interaction.getDisplayPlayerGearLevelParameter() + + val server = when (interaction) { + is GuildChatInputCommandInteraction -> serverRepository.getServer(serverId, interaction.guildId.toString()) + is GlobalChatInputCommandInteraction -> serverRepository.getServer(serverId) + } + + if (server == null) { + interaction.deferEphemeralResponse().respond { + content = "No server with id '$serverId' was found." + } + return + } + + val statusMonitor = server.statusMonitor + if (statusMonitor == null) { + + if (channelId == null) { + interaction.deferEphemeralResponse().respond { + content = "'${ChannelIdParameter.NAME}' is required when using this command for the first time." + } + return + } + + server.statusMonitor = StatusMonitor( + status = status ?: Status.ACTIVE, + discordChannelId = channelId, + displayServerDescription = displayServerDescription ?: true, + displayPlayerGearLevel = displayPlayerGearLevel ?: true + ) + + logger.info("Successfully configured the status monitor for server '$serverId'.") + + interaction.deferEphemeralResponse().respond { + content = """Successfully configured the status monitor for server with id '$serverId'. + |It may take a few minutes for the status embed to appear.""".trimMargin() + } + return + } + + if (channelId != null) { + statusMonitor.discordChannelId = channelId + } + if (status != null) { + statusMonitor.status = status + } + if (displayServerDescription != null) { + statusMonitor.displayServerDescription = displayServerDescription + } + if (displayPlayerGearLevel != null) { + statusMonitor.displayPlayerGearLevel = displayPlayerGearLevel + } + + serverRepository.updateServer(server) + + logger.info("Successfully updated the status monitor for server '$serverId'.") + + interaction.deferEphemeralResponse().respond { + content = """Successfully updated the status monitor for server with id '$serverId'. + |It may take a few minutes for the status embed to reflect the changes.""".trimMargin() + } + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/ErrorHelper.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/ErrorHelper.kt new file mode 100644 index 0000000..7ba6a87 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/ErrorHelper.kt @@ -0,0 +1,19 @@ +package de.darkatra.vrising.discord.commands + +import de.darkatra.vrising.discord.persistence.model.ErrorAware +import dev.kord.rest.builder.message.EmbedBuilder +import org.springframework.util.StringUtils + +fun EmbedBuilder.renderRecentErrors(errorAware: ErrorAware, maxCharactersPerError: Int) { + val recentErrors = errorAware.recentErrors + if (recentErrors.isNotEmpty()) { + recentErrors.chunked(5).forEachIndexed { i, chunk -> + field { + name = "Errors - Page ${i + 1}" + value = chunk.joinToString("\n") { + "```${StringUtils.truncate(it.message, maxCharactersPerError)}```" + } + } + } + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/GetPlayerActivityFeedDetailsCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/GetPlayerActivityFeedDetailsCommand.kt new file mode 100644 index 0000000..21a4636 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/GetPlayerActivityFeedDetailsCommand.kt @@ -0,0 +1,89 @@ +package de.darkatra.vrising.discord.commands + +import de.darkatra.vrising.discord.BotProperties +import de.darkatra.vrising.discord.commands.parameters.addServerIdParameter +import de.darkatra.vrising.discord.persistence.ServerRepository +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.message.embed +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.stereotype.Component + +@Component +@EnableConfigurationProperties(BotProperties::class) +class GetPlayerActivityFeedDetailsCommand( + private val serverRepository: ServerRepository, + private val botProperties: BotProperties +) : Command { + + private val name: String = "get-player-activity-feed-details" + private val description: String = "Gets all details of the player activity feed for the specified server." + + override fun getCommandName(): String = name + + override suspend fun register(kord: Kord) { + + kord.createGlobalChatInputCommand( + name = name, + description = description + ) { + dmPermission = true + disableCommandInGuilds() + + addServerIdParameter() + } + } + + override suspend fun handle(interaction: ChatInputCommandInteraction) { + + val server = interaction.getServer(serverRepository) + ?: return + + val playerActivityFeed = server.playerActivityFeed + if (playerActivityFeed == null) { + interaction.deferEphemeralResponse().respond { + content = "No player activity feed is configured for server with id '${server.id}'." + } + return + } + + interaction.deferEphemeralResponse().respond { + embed { + title = "Player Activity Feed Details for ${server.id}" + + field { + name = "Status" + value = playerActivityFeed.status.name + inline = true + } + + field { + name = "Discord Server Id" + value = server.discordServerId + inline = true + } + + field { + name = "Discord Channel Id" + value = playerActivityFeed.discordChannelId + inline = true + } + + field { + name = "Current Failed Attempts" + value = "${playerActivityFeed.currentFailedAttempts}" + inline = true + } + + field { + name = "Last Updated" + value = "" + inline = true + } + + renderRecentErrors(playerActivityFeed, botProperties.maxCharactersPerError) + } + } + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/GetPvpKillFeedDetailsCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/GetPvpKillFeedDetailsCommand.kt new file mode 100644 index 0000000..b42c1ff --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/GetPvpKillFeedDetailsCommand.kt @@ -0,0 +1,89 @@ +package de.darkatra.vrising.discord.commands + +import de.darkatra.vrising.discord.BotProperties +import de.darkatra.vrising.discord.commands.parameters.addServerIdParameter +import de.darkatra.vrising.discord.persistence.ServerRepository +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.message.embed +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.stereotype.Component + +@Component +@EnableConfigurationProperties(BotProperties::class) +class GetPvpKillFeedDetailsCommand( + private val serverRepository: ServerRepository, + private val botProperties: BotProperties +) : Command { + + private val name: String = "get-pvp-kill-feed-details" + private val description: String = "Gets all details of the pvp kill feed for the specified server." + + override fun getCommandName(): String = name + + override suspend fun register(kord: Kord) { + + kord.createGlobalChatInputCommand( + name = name, + description = description + ) { + dmPermission = true + disableCommandInGuilds() + + addServerIdParameter() + } + } + + override suspend fun handle(interaction: ChatInputCommandInteraction) { + + val server = interaction.getServer(serverRepository) + ?: return + + val pvpKillFeed = server.pvpKillFeed + if (pvpKillFeed == null) { + interaction.deferEphemeralResponse().respond { + content = "No pvp kill feed is configured for server with id '${server.id}'." + } + return + } + + interaction.deferEphemeralResponse().respond { + embed { + title = "Pvp Kill Feed Details for ${server.id}" + + field { + name = "Status" + value = pvpKillFeed.status.name + inline = true + } + + field { + name = "Discord Server Id" + value = server.discordServerId + inline = true + } + + field { + name = "Discord Channel Id" + value = pvpKillFeed.discordChannelId + inline = true + } + + field { + name = "Current Failed Attempts" + value = "${pvpKillFeed.currentFailedAttempts}" + inline = true + } + + field { + name = "Last Updated" + value = "" + inline = true + } + + renderRecentErrors(pvpKillFeed, botProperties.maxCharactersPerError) + } + } + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/GetServerDetailsCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/GetServerDetailsCommand.kt index 13f15c7..3daa6e5 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/GetServerDetailsCommand.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/GetServerDetailsCommand.kt @@ -1,24 +1,18 @@ package de.darkatra.vrising.discord.commands -import de.darkatra.vrising.discord.BotProperties -import de.darkatra.vrising.discord.commands.parameters.addServerStatusMonitorIdParameter -import de.darkatra.vrising.discord.commands.parameters.getServerStatusMonitorIdParameter -import de.darkatra.vrising.discord.persistence.ServerStatusMonitorRepository +import de.darkatra.vrising.discord.commands.parameters.addServerIdParameter +import de.darkatra.vrising.discord.persistence.ServerRepository import dev.kord.core.Kord import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.entity.interaction.ChatInputCommandInteraction -import dev.kord.core.entity.interaction.GlobalChatInputCommandInteraction -import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction import dev.kord.rest.builder.message.embed -import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.stereotype.Component -import org.springframework.util.StringUtils +import java.util.Objects +import java.util.stream.Stream @Component -@EnableConfigurationProperties(BotProperties::class) class GetServerDetailsCommand( - private val serverStatusMonitorRepository: ServerStatusMonitorRepository, - private val botProperties: BotProperties + private val serverRepository: ServerRepository ) : Command { private val name: String = "get-server-details" @@ -35,52 +29,41 @@ class GetServerDetailsCommand( dmPermission = true disableCommandInGuilds() - addServerStatusMonitorIdParameter() + addServerIdParameter() } } override suspend fun handle(interaction: ChatInputCommandInteraction) { - val serverStatusMonitorId = interaction.getServerStatusMonitorIdParameter() - - val serverStatusMonitor = when (interaction) { - is GuildChatInputCommandInteraction -> serverStatusMonitorRepository.getServerStatusMonitor(serverStatusMonitorId, interaction.guildId.toString()) - is GlobalChatInputCommandInteraction -> serverStatusMonitorRepository.getServerStatusMonitor(serverStatusMonitorId) - } - - if (serverStatusMonitor == null) { - interaction.deferEphemeralResponse().respond { - content = "No server with id '$serverStatusMonitorId' was found." - } - return - } + val server = interaction.getServer(serverRepository) + ?: return interaction.deferEphemeralResponse().respond { embed { - title = "Details for ${serverStatusMonitor.id}" + title = "Details for ${server.id}" field { name = "Hostname" - value = serverStatusMonitor.hostname + value = server.hostname inline = true } field { name = "Query Port" - value = "${serverStatusMonitor.queryPort}" + value = "${server.queryPort}" inline = true } field { - name = "Status" - value = serverStatusMonitor.status.name + name = "Discord Server Id" + value = server.discordServerId inline = true } field { name = "Api Hostname" - value = when (serverStatusMonitor.apiHostname != null) { - true -> "${serverStatusMonitor.apiHostname}" + value = when (server.apiHostname != null) { + true -> "${server.apiHostname}" false -> "-" } inline = true @@ -88,89 +71,42 @@ class GetServerDetailsCommand( field { name = "Api Port" - value = when (serverStatusMonitor.apiPort != null) { - true -> "${serverStatusMonitor.apiPort}" + value = when (server.apiPort != null) { + true -> "${server.apiPort}" false -> "-" } inline = true } field { - name = "Embed Enabled" - value = "${serverStatusMonitor.embedEnabled}" - inline = true - } - - field { - name = "Display Server Description" - value = "${serverStatusMonitor.displayServerDescription}" - inline = true - } - - field { - name = "Display Player Gear Level" - value = "${serverStatusMonitor.displayPlayerGearLevel}" - inline = true - } - - field { - name = "Discord Server Id" - value = serverStatusMonitor.discordServerId - inline = true - } - - field { - name = "Discord Channel Id" - value = serverStatusMonitor.discordChannelId - inline = true - } - - field { - name = "Player Activity Feed Channel Id" - value = serverStatusMonitor.playerActivityDiscordChannelId ?: "-" + name = "Last Update Attempt" + value = "" inline = true } field { - name = "Pvp Kill Feed Channel Id" - value = serverStatusMonitor.pvpKillFeedDiscordChannelId ?: "-" + name = "Status Monitor Status" + value = server.statusMonitor?.status?.name ?: "-" inline = true } field { - name = "Current Embed Message Id" - value = serverStatusMonitor.currentEmbedMessageId ?: "-" + name = "Player Activity Feed Status" + value = server.playerActivityFeed?.status?.name ?: "-" inline = true } field { - name = "Current Failed Attempts" - value = "${serverStatusMonitor.currentFailedAttempts}" + name = "Pvp Kill Feed Status" + value = server.pvpKillFeed?.status?.name ?: "-" inline = true } field { - name = "Current Failed Api Attempts" - value = "${serverStatusMonitor.currentFailedApiAttempts}" + name = "Number of Leaderboards" + value = "${Stream.of(server.pvpKillFeed).filter(Objects::nonNull).count()}" inline = true } - - field { - name = "Last Update Attempt" - value = "" - inline = true - } - - if (serverStatusMonitor.recentErrors.isNotEmpty()) { - serverStatusMonitor.recentErrors.chunked(5).forEachIndexed { i, chunk -> - field { - name = "Most recent Errors $i" - value = chunk.joinToString("\n") { - "```${StringUtils.truncate(it.message, botProperties.maxCharactersPerError)}```" - } - } - } - } } } } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/GetStatusMonitorDetailsCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/GetStatusMonitorDetailsCommand.kt new file mode 100644 index 0000000..c6b04ed --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/GetStatusMonitorDetailsCommand.kt @@ -0,0 +1,113 @@ +package de.darkatra.vrising.discord.commands + +import de.darkatra.vrising.discord.BotProperties +import de.darkatra.vrising.discord.commands.parameters.addServerIdParameter +import de.darkatra.vrising.discord.persistence.ServerRepository +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.message.embed +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.stereotype.Component + +@Component +@EnableConfigurationProperties(BotProperties::class) +class GetStatusMonitorDetailsCommand( + private val serverRepository: ServerRepository, + private val botProperties: BotProperties +) : Command { + + private val name: String = "get-status-monitor-details" + private val description: String = "Gets all details of the status monitor for the specified server." + + override fun getCommandName(): String = name + + override suspend fun register(kord: Kord) { + + kord.createGlobalChatInputCommand( + name = name, + description = description + ) { + dmPermission = true + disableCommandInGuilds() + + addServerIdParameter() + } + } + + override suspend fun handle(interaction: ChatInputCommandInteraction) { + + val server = interaction.getServer(serverRepository) + ?: return + + val statusMonitor = server.statusMonitor + if (statusMonitor == null) { + interaction.deferEphemeralResponse().respond { + content = "No status monitor is configured for server with id '${server.id}'." + } + return + } + + interaction.deferEphemeralResponse().respond { + embed { + title = "Status Monitor Details for ${server.id}" + + field { + name = "Status" + value = statusMonitor.status.name + inline = true + } + + field { + name = "Discord Server Id" + value = server.discordServerId + inline = true + } + + field { + name = "Discord Channel Id" + value = statusMonitor.discordChannelId + inline = true + } + + field { + name = "Display Server Description" + value = "${statusMonitor.displayServerDescription}" + inline = true + } + + field { + name = "Display Player Gear Level" + value = "${statusMonitor.displayPlayerGearLevel}" + inline = true + } + + field { + name = "Current Embed Message Id" + value = statusMonitor.currentEmbedMessageId ?: "-" + inline = true + } + + field { + name = "Current Failed Attempts" + value = "${statusMonitor.currentFailedAttempts}" + inline = true + } + + field { + name = "Current Failed Api Attempts" + value = "${statusMonitor.currentFailedApiAttempts}" + inline = true + } + + field { + name = "Last Updated" + value = "" + inline = true + } + + renderRecentErrors(statusMonitor, botProperties.maxCharactersPerError) + } + } + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/ListServersCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/ListServersCommand.kt index 675ee5e..c927e89 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/ListServersCommand.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/ListServersCommand.kt @@ -4,8 +4,8 @@ import de.darkatra.vrising.discord.BotProperties import de.darkatra.vrising.discord.commands.parameters.PageParameter import de.darkatra.vrising.discord.commands.parameters.addPageParameter import de.darkatra.vrising.discord.commands.parameters.getPageParameter -import de.darkatra.vrising.discord.persistence.ServerStatusMonitorRepository -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor +import de.darkatra.vrising.discord.persistence.ServerRepository +import de.darkatra.vrising.discord.persistence.model.Server import dev.kord.core.Kord import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.entity.interaction.ChatInputCommandInteraction @@ -19,11 +19,11 @@ private const val PAGE_SIZE = 10 @Component @EnableConfigurationProperties(BotProperties::class) class ListServersCommand( - private val serverStatusMonitorRepository: ServerStatusMonitorRepository + private val serverRepository: ServerRepository ) : Command { private val name: String = "list-servers" - private val description: String = "Lists all server status monitors." + private val description: String = "Lists all servers." override fun getCommandName(): String = name @@ -46,8 +46,8 @@ class ListServersCommand( PageParameter.validate(page) val totalElements = when (interaction) { - is GuildChatInputCommandInteraction -> serverStatusMonitorRepository.count(interaction.guildId.toString()) - is GlobalChatInputCommandInteraction -> serverStatusMonitorRepository.count() + is GuildChatInputCommandInteraction -> serverRepository.count(interaction.guildId.toString()) + is GlobalChatInputCommandInteraction -> serverRepository.count() } val totalPages = totalElements / PAGE_SIZE + 1 @@ -58,24 +58,24 @@ class ListServersCommand( return } - val serverStatusMonitors: List = when (interaction) { - is GuildChatInputCommandInteraction -> serverStatusMonitorRepository.getServerStatusMonitors( + val servers: List = when (interaction) { + is GuildChatInputCommandInteraction -> serverRepository.getServers( discordServerId = interaction.guildId.toString(), offset = page * PAGE_SIZE, limit = PAGE_SIZE ) - is GlobalChatInputCommandInteraction -> serverStatusMonitorRepository.getServerStatusMonitors( + is GlobalChatInputCommandInteraction -> serverRepository.getServers( offset = page * PAGE_SIZE, limit = PAGE_SIZE ) } interaction.deferEphemeralResponse().respond { - content = when (serverStatusMonitors.isEmpty()) { + content = when (servers.isEmpty()) { true -> "No servers found." - false -> serverStatusMonitors.joinToString(separator = "\n") { serverStatusMonitor -> - "${serverStatusMonitor.id} - ${serverStatusMonitor.hostname}:${serverStatusMonitor.queryPort} - ${serverStatusMonitor.status.name}" + false -> servers.joinToString(separator = "\n") { server -> + "${server.id} - ${server.hostname}:${server.queryPort} - ${server.status.name}" } + "\n*Current Page: $page, Total Pages: $totalPages*" } } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/RemoveServerCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/RemoveServerCommand.kt index cbfa77c..7677d9b 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/RemoveServerCommand.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/RemoveServerCommand.kt @@ -1,8 +1,8 @@ package de.darkatra.vrising.discord.commands -import de.darkatra.vrising.discord.commands.parameters.addServerStatusMonitorIdParameter -import de.darkatra.vrising.discord.commands.parameters.getServerStatusMonitorIdParameter -import de.darkatra.vrising.discord.persistence.ServerStatusMonitorRepository +import de.darkatra.vrising.discord.commands.parameters.addServerIdParameter +import de.darkatra.vrising.discord.commands.parameters.getServerIdParameter +import de.darkatra.vrising.discord.persistence.ServerRepository import dev.kord.core.Kord import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.entity.interaction.ChatInputCommandInteraction @@ -13,13 +13,13 @@ import org.springframework.stereotype.Component @Component class RemoveServerCommand( - private val serverStatusMonitorRepository: ServerStatusMonitorRepository + private val serverRepository: ServerRepository ) : Command { private val logger = LoggerFactory.getLogger(javaClass) private val name: String = "remove-server" - private val description: String = "Removes a server from the status monitor." + private val description: String = "Removes a server." override fun getCommandName(): String = name @@ -32,29 +32,27 @@ class RemoveServerCommand( dmPermission = true disableCommandInGuilds() - addServerStatusMonitorIdParameter() + addServerIdParameter() } } override suspend fun handle(interaction: ChatInputCommandInteraction) { - val serverStatusMonitorId = interaction.getServerStatusMonitorIdParameter() + val serverId = interaction.getServerIdParameter() val wasSuccessful = when (interaction) { - is GuildChatInputCommandInteraction -> serverStatusMonitorRepository.removeServerStatusMonitor( - serverStatusMonitorId, - interaction.guildId.toString() - ) - - is GlobalChatInputCommandInteraction -> serverStatusMonitorRepository.removeServerStatusMonitor(serverStatusMonitorId) + is GuildChatInputCommandInteraction -> serverRepository.removeServer(serverId, interaction.guildId.toString()) + is GlobalChatInputCommandInteraction -> serverRepository.removeServer(serverId) } - logger.info("Successfully removed monitor with id '${serverStatusMonitorId}'.") + if (wasSuccessful) { + logger.info("Successfully removed server with id '$serverId'.") + } interaction.deferEphemeralResponse().respond { content = when (wasSuccessful) { - true -> "Removed monitor with id '$serverStatusMonitorId'." - false -> "No server with id '$serverStatusMonitorId' was found." + true -> "Removed server with id '$serverId'." + false -> "No server with id '$serverId' was found." } } } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/ServerHelper.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/ServerHelper.kt new file mode 100644 index 0000000..d81aae4 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/ServerHelper.kt @@ -0,0 +1,28 @@ +package de.darkatra.vrising.discord.commands + +import de.darkatra.vrising.discord.commands.parameters.getServerIdParameter +import de.darkatra.vrising.discord.persistence.ServerRepository +import de.darkatra.vrising.discord.persistence.model.Server +import dev.kord.core.behavior.interaction.response.respond +import dev.kord.core.entity.interaction.ChatInputCommandInteraction +import dev.kord.core.entity.interaction.GlobalChatInputCommandInteraction +import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction + +suspend fun ChatInputCommandInteraction.getServer(serverRepository: ServerRepository): Server? { + + val serverId = getServerIdParameter() + + val server = when (this) { + is GuildChatInputCommandInteraction -> serverRepository.getServer(serverId, guildId.toString()) + is GlobalChatInputCommandInteraction -> serverRepository.getServer(serverId) + } + + if (server == null) { + deferEphemeralResponse().respond { + content = "No server with id '$serverId' was found." + } + return null + } + + return server +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/UpdateServerCommand.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/UpdateServerCommand.kt index e26ae29..66818b1 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/UpdateServerCommand.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/UpdateServerCommand.kt @@ -3,38 +3,24 @@ package de.darkatra.vrising.discord.commands import de.darkatra.vrising.discord.BotProperties import de.darkatra.vrising.discord.commands.parameters.ServerApiHostnameParameter import de.darkatra.vrising.discord.commands.parameters.ServerHostnameParameter -import de.darkatra.vrising.discord.commands.parameters.addDisplayPlayerGearLevelParameter -import de.darkatra.vrising.discord.commands.parameters.addDisplayServerDescriptionParameter -import de.darkatra.vrising.discord.commands.parameters.addEmbedEnabledParameter -import de.darkatra.vrising.discord.commands.parameters.addPlayerActivityFeedChannelIdParameter -import de.darkatra.vrising.discord.commands.parameters.addPvpKillFeedChannelIdParameter import de.darkatra.vrising.discord.commands.parameters.addServerApiHostnameParameter import de.darkatra.vrising.discord.commands.parameters.addServerApiPasswordParameter import de.darkatra.vrising.discord.commands.parameters.addServerApiPortParameter import de.darkatra.vrising.discord.commands.parameters.addServerApiUsernameParameter import de.darkatra.vrising.discord.commands.parameters.addServerHostnameParameter +import de.darkatra.vrising.discord.commands.parameters.addServerIdParameter import de.darkatra.vrising.discord.commands.parameters.addServerQueryPortParameter -import de.darkatra.vrising.discord.commands.parameters.addServerStatusMonitorIdParameter -import de.darkatra.vrising.discord.commands.parameters.addServerStatusMonitorStatusParameter -import de.darkatra.vrising.discord.commands.parameters.getDisplayPlayerGearLevelParameter -import de.darkatra.vrising.discord.commands.parameters.getDisplayServerDescriptionParameter -import de.darkatra.vrising.discord.commands.parameters.getEmbedEnabledParameter -import de.darkatra.vrising.discord.commands.parameters.getPlayerActivityFeedChannelIdParameter -import de.darkatra.vrising.discord.commands.parameters.getPvpKillFeedChannelIdParameter import de.darkatra.vrising.discord.commands.parameters.getServerApiHostnameParameter import de.darkatra.vrising.discord.commands.parameters.getServerApiPasswordParameter import de.darkatra.vrising.discord.commands.parameters.getServerApiPortParameter import de.darkatra.vrising.discord.commands.parameters.getServerApiUsernameParameter import de.darkatra.vrising.discord.commands.parameters.getServerHostnameParameter +import de.darkatra.vrising.discord.commands.parameters.getServerIdParameter import de.darkatra.vrising.discord.commands.parameters.getServerQueryPortParameter -import de.darkatra.vrising.discord.commands.parameters.getServerStatusMonitorIdParameter -import de.darkatra.vrising.discord.commands.parameters.getServerStatusMonitorStatusParameter -import de.darkatra.vrising.discord.persistence.ServerStatusMonitorRepository +import de.darkatra.vrising.discord.persistence.ServerRepository import dev.kord.core.Kord import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.entity.interaction.ChatInputCommandInteraction -import dev.kord.core.entity.interaction.GlobalChatInputCommandInteraction -import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction import org.slf4j.LoggerFactory import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.stereotype.Component @@ -42,14 +28,14 @@ import org.springframework.stereotype.Component @Component @EnableConfigurationProperties(BotProperties::class) class UpdateServerCommand( - private val serverStatusMonitorRepository: ServerStatusMonitorRepository, + private val serverRepository: ServerRepository, private val botProperties: BotProperties ) : Command { private val logger = LoggerFactory.getLogger(javaClass) private val name: String = "update-server" - private val description: String = "Updates the given server status monitor." + private val description: String = "Updates the given server." override fun getCommandName(): String = name @@ -62,7 +48,7 @@ class UpdateServerCommand( dmPermission = true disableCommandInGuilds() - addServerStatusMonitorIdParameter() + addServerIdParameter() addServerHostnameParameter(required = false) addServerQueryPortParameter(required = false) @@ -71,21 +57,15 @@ class UpdateServerCommand( addServerApiPortParameter(required = false) addServerApiUsernameParameter(required = false) addServerApiPasswordParameter(required = false) - - addServerStatusMonitorStatusParameter(required = false) - - addEmbedEnabledParameter(required = false) - addDisplayServerDescriptionParameter(required = false) - addDisplayPlayerGearLevelParameter(required = false) - - addPlayerActivityFeedChannelIdParameter(required = false) - addPvpKillFeedChannelIdParameter(required = false) } } override suspend fun handle(interaction: ChatInputCommandInteraction) { - val serverStatusMonitorId = interaction.getServerStatusMonitorIdParameter() + val server = interaction.getServer(serverRepository) + ?: return + + val serverId = interaction.getServerIdParameter() val hostname = interaction.getServerHostnameParameter() val queryPort = interaction.getServerQueryPortParameter() @@ -94,73 +74,35 @@ class UpdateServerCommand( val apiUsername = interaction.getServerApiUsernameParameter() val apiPassword = interaction.getServerApiPasswordParameter() - val status = interaction.getServerStatusMonitorStatusParameter() - - val embedEnabled = interaction.getEmbedEnabledParameter() - val displayServerDescription = interaction.getDisplayServerDescriptionParameter() - val displayPlayerGearLevel = interaction.getDisplayPlayerGearLevelParameter() - - val playerActivityFeedChannelId = interaction.getPlayerActivityFeedChannelIdParameter() - val pvpKillFeedChannelId = interaction.getPvpKillFeedChannelIdParameter() - - val serverStatusMonitor = when (interaction) { - is GuildChatInputCommandInteraction -> serverStatusMonitorRepository.getServerStatusMonitor(serverStatusMonitorId, interaction.guildId.toString()) - is GlobalChatInputCommandInteraction -> serverStatusMonitorRepository.getServerStatusMonitor(serverStatusMonitorId) - } - - if (serverStatusMonitor == null) { - interaction.deferEphemeralResponse().respond { - content = "No server with id '$serverStatusMonitorId' was found." - } - return - } - if (hostname != null) { ServerHostnameParameter.validate(hostname, botProperties.allowLocalAddressRanges) - serverStatusMonitor.hostname = hostname + server.hostname = hostname } if (queryPort != null) { - serverStatusMonitor.queryPort = queryPort + server.queryPort = queryPort } if (apiHostname != null) { - serverStatusMonitor.apiHostname = determineValueOfNullableStringParameter(apiHostname).also { + server.apiHostname = determineValueOfNullableStringParameter(apiHostname).also { ServerApiHostnameParameter.validate(it, botProperties.allowLocalAddressRanges) } } if (apiPort != null) { - serverStatusMonitor.apiPort = if (apiPort == -1) null else apiPort + server.apiPort = if (apiPort == -1) null else apiPort } if (apiUsername != null) { - serverStatusMonitor.apiUsername = determineValueOfNullableStringParameter(apiUsername) + server.apiUsername = determineValueOfNullableStringParameter(apiUsername) } if (apiPassword != null) { - serverStatusMonitor.apiPassword = determineValueOfNullableStringParameter(apiPassword) - } - if (status != null) { - serverStatusMonitor.status = status - } - if (embedEnabled != null) { - serverStatusMonitor.embedEnabled = embedEnabled - } - if (displayServerDescription != null) { - serverStatusMonitor.displayServerDescription = displayServerDescription - } - if (displayPlayerGearLevel != null) { - serverStatusMonitor.displayPlayerGearLevel = displayPlayerGearLevel - } - if (playerActivityFeedChannelId != null) { - serverStatusMonitor.playerActivityDiscordChannelId = determineValueOfNullableStringParameter(playerActivityFeedChannelId) - } - if (pvpKillFeedChannelId != null) { - serverStatusMonitor.pvpKillFeedDiscordChannelId = determineValueOfNullableStringParameter(pvpKillFeedChannelId) + server.apiPassword = determineValueOfNullableStringParameter(apiPassword) } - serverStatusMonitorRepository.updateServerStatusMonitor(serverStatusMonitor) + serverRepository.updateServer(server) - logger.info("Successfully updated monitor with id '${serverStatusMonitorId}'.") + logger.info("Successfully updated server '$serverId'.") interaction.deferEphemeralResponse().respond { - content = "Updated server status monitor with id '${serverStatusMonitorId}'. It may take some time until the status message is updated." + content = """Successfully updated server with id '$serverId'. + |Related status embeds, activity feeds, kill feeds and leaderboards may take some time to reflect the changes.""".trimMargin() } } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ChannelIdParameter.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ChannelIdParameter.kt new file mode 100644 index 0000000..0ec8b13 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ChannelIdParameter.kt @@ -0,0 +1,23 @@ +package de.darkatra.vrising.discord.commands.parameters + +import de.darkatra.vrising.discord.getChannelIdFromStringParameter +import dev.kord.core.entity.interaction.ChatInputCommandInteraction +import dev.kord.rest.builder.interaction.GlobalChatInputCreateBuilder +import dev.kord.rest.builder.interaction.string + +object ChannelIdParameter { + const val NAME = "channel-id" +} + +fun GlobalChatInputCreateBuilder.addChannelIdParameter(required: Boolean = true) { + string( + name = ChannelIdParameter.NAME, + description = "The id of the channel to post to." + ) { + this.required = required + } +} + +fun ChatInputCommandInteraction.getChannelIdParameter(): String? { + return command.getChannelIdFromStringParameter(ChannelIdParameter.NAME) +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/EmbedEnabledParameter.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/EmbedEnabledParameter.kt deleted file mode 100644 index 03da07d..0000000 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/EmbedEnabledParameter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package de.darkatra.vrising.discord.commands.parameters - -import dev.kord.core.entity.interaction.ChatInputCommandInteraction -import dev.kord.rest.builder.interaction.GlobalChatInputCreateBuilder -import dev.kord.rest.builder.interaction.boolean - -object EmbedEnabledParameter { - const val NAME = "embed-enabled" -} - -fun GlobalChatInputCreateBuilder.addEmbedEnabledParameter(required: Boolean = true) { - boolean( - name = EmbedEnabledParameter.NAME, - description = "Whether or not a discord status embed should be posted. Defaults to true." - ) { - this.required = required - } -} - -fun ChatInputCommandInteraction.getEmbedEnabledParameter(): Boolean? { - return command.booleans[EmbedEnabledParameter.NAME] -} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/PlayerActivityFeedChannelIdParameter.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/PlayerActivityFeedChannelIdParameter.kt deleted file mode 100644 index c6f7b19..0000000 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/PlayerActivityFeedChannelIdParameter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package de.darkatra.vrising.discord.commands.parameters - -import de.darkatra.vrising.discord.getChannelIdFromStringParameter -import dev.kord.core.entity.interaction.ChatInputCommandInteraction -import dev.kord.rest.builder.interaction.GlobalChatInputCreateBuilder -import dev.kord.rest.builder.interaction.string - -object PlayerActivityFeedChannelIdParameter { - const val NAME = "player-activity-feed-channel-id" -} - -fun GlobalChatInputCreateBuilder.addPlayerActivityFeedChannelIdParameter(required: Boolean = true) { - string( - name = PlayerActivityFeedChannelIdParameter.NAME, - description = "The id of the channel to post the player activity feed in." - ) { - this.required = required - } -} - -fun ChatInputCommandInteraction.getPlayerActivityFeedChannelIdParameter(): String? { - return command.getChannelIdFromStringParameter(PlayerActivityFeedChannelIdParameter.NAME) -} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/PvpKillFeedChannelIdParameter.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/PvpKillFeedChannelIdParameter.kt deleted file mode 100644 index ffbf8f0..0000000 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/PvpKillFeedChannelIdParameter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package de.darkatra.vrising.discord.commands.parameters - -import de.darkatra.vrising.discord.getChannelIdFromStringParameter -import dev.kord.core.entity.interaction.ChatInputCommandInteraction -import dev.kord.rest.builder.interaction.GlobalChatInputCreateBuilder -import dev.kord.rest.builder.interaction.string - -object PvpKillFeedChannelIdParameter { - const val NAME = "pvp-kill-feed-channel-id" -} - -fun GlobalChatInputCreateBuilder.addPvpKillFeedChannelIdParameter(required: Boolean = true) { - string( - name = PvpKillFeedChannelIdParameter.NAME, - description = "The id of the channel to post the pvp kill feed in." - ) { - this.required = required - } -} - -fun ChatInputCommandInteraction.getPvpKillFeedChannelIdParameter(): String? { - return command.getChannelIdFromStringParameter(PvpKillFeedChannelIdParameter.NAME) -} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ServerIdParameter.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ServerIdParameter.kt new file mode 100644 index 0000000..039814c --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ServerIdParameter.kt @@ -0,0 +1,22 @@ +package de.darkatra.vrising.discord.commands.parameters + +import dev.kord.core.entity.interaction.ChatInputCommandInteraction +import dev.kord.rest.builder.interaction.GlobalChatInputCreateBuilder +import dev.kord.rest.builder.interaction.string + +object ServerIdParameter { + const val NAME = "server-id" +} + +fun GlobalChatInputCreateBuilder.addServerIdParameter() { + string( + name = ServerIdParameter.NAME, + description = "The id of the server." + ) { + required = true + } +} + +fun ChatInputCommandInteraction.getServerIdParameter(): String { + return command.strings[ServerIdParameter.NAME]!! +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ServerStatusMonitorIdParameter.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ServerStatusMonitorIdParameter.kt deleted file mode 100644 index e530e26..0000000 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ServerStatusMonitorIdParameter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package de.darkatra.vrising.discord.commands.parameters - -import dev.kord.core.entity.interaction.ChatInputCommandInteraction -import dev.kord.rest.builder.interaction.GlobalChatInputCreateBuilder -import dev.kord.rest.builder.interaction.string - -object ServerStatusMonitorIdParameter { - const val NAME = "server-status-monitor-id" -} - -fun GlobalChatInputCreateBuilder.addServerStatusMonitorIdParameter() { - string( - name = ServerStatusMonitorIdParameter.NAME, - description = "The id of the server status monitor." - ) { - required = true - } -} - -fun ChatInputCommandInteraction.getServerStatusMonitorIdParameter(): String { - return command.strings[ServerStatusMonitorIdParameter.NAME]!! -} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ServerStatusMonitorStatusParameter.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ServerStatusMonitorStatusParameter.kt deleted file mode 100644 index 655facb..0000000 --- a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/ServerStatusMonitorStatusParameter.kt +++ /dev/null @@ -1,26 +0,0 @@ -package de.darkatra.vrising.discord.commands.parameters - -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitorStatus -import dev.kord.core.entity.interaction.ChatInputCommandInteraction -import dev.kord.rest.builder.interaction.GlobalChatInputCreateBuilder -import dev.kord.rest.builder.interaction.string - -object ServerStatusMonitorStatusParameter { - const val NAME = "status" -} - -fun GlobalChatInputCreateBuilder.addServerStatusMonitorStatusParameter(required: Boolean = true) { - string( - name = ServerStatusMonitorStatusParameter.NAME, - description = "Determines if a server status monitor should be updated or not." - ) { - this.required = required - - choice("ACTIVE", "ACTIVE") - choice("INACTIVE", "INACTIVE") - } -} - -fun ChatInputCommandInteraction.getServerStatusMonitorStatusParameter(): ServerStatusMonitorStatus? { - return command.strings[ServerStatusMonitorStatusParameter.NAME]?.let { ServerStatusMonitorStatus.valueOf(it) } -} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/StatusParameter.kt b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/StatusParameter.kt new file mode 100644 index 0000000..1f7d59e --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/commands/parameters/StatusParameter.kt @@ -0,0 +1,26 @@ +package de.darkatra.vrising.discord.commands.parameters + +import de.darkatra.vrising.discord.persistence.model.Status +import dev.kord.core.entity.interaction.ChatInputCommandInteraction +import dev.kord.rest.builder.interaction.GlobalChatInputCreateBuilder +import dev.kord.rest.builder.interaction.string + +object StatusParameter { + const val NAME = "status" +} + +fun GlobalChatInputCreateBuilder.addStatusParameter(required: Boolean = true) { + string( + name = StatusParameter.NAME, + description = "Determines if a feature is active or not." + ) { + this.required = required + + choice("ACTIVE", "ACTIVE") + choice("INACTIVE", "INACTIVE") + } +} + +fun ChatInputCommandInteraction.getStatusParameter(): Status? { + return command.strings[StatusParameter.NAME]?.let { Status.valueOf(it) } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigration.kt b/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigration.kt index 68abb23..69d8ee3 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigration.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigration.kt @@ -6,6 +6,7 @@ import org.dizitart.no2.Nitrite class DatabaseMigration( val description: String, val isApplicable: (currentSchemaVersion: SemanticVersion) -> Boolean, + val documentCollectionName: String, val documentAction: (document: Document) -> Unit = {}, val databaseAction: (database: Nitrite) -> Unit = {} ) diff --git a/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigrationService.kt b/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigrationService.kt index 94ebe9b..19042f7 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigrationService.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigrationService.kt @@ -1,11 +1,9 @@ package de.darkatra.vrising.discord.migration -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitorStatus +import de.darkatra.vrising.discord.persistence.model.Status import org.dizitart.no2.Document import org.dizitart.no2.Nitrite import org.dizitart.no2.objects.filters.ObjectFilters -import org.dizitart.no2.util.ObjectUtils import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service @@ -26,19 +24,22 @@ class DatabaseMigrationService( DatabaseMigration( description = "Set default value for displayPlayerGearLevel property.", isApplicable = { currentSchemaVersion -> currentSchemaVersion.major == 1 && currentSchemaVersion.minor <= 3 }, + documentCollectionName = "de.darkatra.vrising.discord.ServerStatusMonitor", documentAction = { document -> document["displayPlayerGearLevel"] = true } ), DatabaseMigration( description = "Set default value for status and displayServerDescription property.", isApplicable = { currentSchemaVersion -> currentSchemaVersion.major == 1 && currentSchemaVersion.minor <= 4 }, + documentCollectionName = "de.darkatra.vrising.discord.ServerStatusMonitor", documentAction = { document -> - document["status"] = ServerStatusMonitorStatus.ACTIVE.name + document["status"] = Status.ACTIVE.name document["displayServerDescription"] = true } ), DatabaseMigration( description = "Remove the displayPlayerGearLevel property due to patch 0.5.42405.", isApplicable = { currentSchemaVersion -> currentSchemaVersion.major == 1 && currentSchemaVersion.minor <= 5 }, + documentCollectionName = "de.darkatra.vrising.discord.ServerStatusMonitor", documentAction = { document -> // we can't remove the field completely due to how nitrites update function works // setting it to false instead (this was the default value in previous versions) @@ -48,14 +49,16 @@ class DatabaseMigrationService( DatabaseMigration( description = "Set default value for currentFailedAttempts property.", isApplicable = { currentSchemaVersion -> currentSchemaVersion.major == 1 && currentSchemaVersion.minor <= 7 }, + documentCollectionName = "de.darkatra.vrising.discord.ServerStatusMonitor", documentAction = { document -> document["currentFailedAttempts"] = 0 } ), DatabaseMigration( description = "Migrate the existing ServerStatusMonitor collection to the new collection name introduced by a package change and set defaults for displayClan, displayGearLevel and displayKilledVBloods.", isApplicable = { currentSchemaVersion -> currentSchemaVersion.major < 2 || (currentSchemaVersion.major == 2 && currentSchemaVersion.minor <= 1) }, + documentCollectionName = "de.darkatra.vrising.discord.serverstatus.model.ServerStatusMonitor", databaseAction = { database -> val oldCollection = database.getCollection("de.darkatra.vrising.discord.ServerStatusMonitor") - val newCollection = database.getCollection(ObjectUtils.findObjectStoreName(ServerStatusMonitor::class.java)) + val newCollection = database.getCollection("de.darkatra.vrising.discord.serverstatus.model.ServerStatusMonitor") oldCollection.find().forEach { document -> newCollection.insert(document) } @@ -69,6 +72,7 @@ class DatabaseMigrationService( DatabaseMigration( description = "Set default value for version property.", isApplicable = { currentSchemaVersion -> currentSchemaVersion.major < 2 || (currentSchemaVersion.major == 2 && currentSchemaVersion.minor <= 2) }, + documentCollectionName = "de.darkatra.vrising.discord.serverstatus.model.ServerStatusMonitor", documentAction = { document -> document["version"] = Instant.now().toEpochMilli() } @@ -76,6 +80,7 @@ class DatabaseMigrationService( DatabaseMigration( description = "Make it possible to disable the discord embed and only use the activity or kill feed.", isApplicable = { currentSchemaVersion -> currentSchemaVersion.major < 2 || (currentSchemaVersion.major == 2 && currentSchemaVersion.minor <= 8) }, + documentCollectionName = "de.darkatra.vrising.discord.serverstatus.model.ServerStatusMonitor", documentAction = { document -> document["embedEnabled"] = true } @@ -83,6 +88,7 @@ class DatabaseMigrationService( DatabaseMigration( description = "Serialize error timestamp as long (epochSecond).", isApplicable = { currentSchemaVersion -> currentSchemaVersion.major < 2 || (currentSchemaVersion.major == 2 && currentSchemaVersion.minor <= 9) }, + documentCollectionName = "de.darkatra.vrising.discord.serverstatus.model.ServerStatusMonitor", documentAction = { document -> val recentErrors = document["recentErrors"] if (recentErrors is List<*>) { @@ -99,14 +105,75 @@ class DatabaseMigrationService( DatabaseMigration( description = "Migrate the existing ServerStatusMonitor collection to the new collection name introduced by a package change.", isApplicable = { currentSchemaVersion -> currentSchemaVersion.major < 2 || (currentSchemaVersion.major == 2 && currentSchemaVersion.minor <= 10 && currentSchemaVersion.patch <= 1) }, + documentCollectionName = "de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor", databaseAction = { database -> val oldCollection = database.getCollection("de.darkatra.vrising.discord.serverstatus.model.ServerStatusMonitor") - val newCollection = database.getCollection(ObjectUtils.findObjectStoreName(ServerStatusMonitor::class.java)) + val newCollection = database.getCollection("de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor") oldCollection.find().forEach { document -> newCollection.insert(document) } oldCollection.remove(ObjectFilters.ALL) } + ), + DatabaseMigration( + description = "Each feature now has its own nested database object. Will not migrate previous errors to the new format.", + isApplicable = { currentSchemaVersion -> currentSchemaVersion.major < 2 || (currentSchemaVersion.major == 2 && currentSchemaVersion.minor <= 10) }, + documentCollectionName = "de.darkatra.vrising.discord.persistence.model.Server", + databaseAction = { database -> + val oldCollection = database.getCollection("de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor") + val newCollection = database.getCollection("de.darkatra.vrising.discord.persistence.model.Server") + oldCollection.find().forEach { document -> + + val server = Document().apply { + put("id", document["id"]) + put("version", document["version"]) + put("discordServerId", document["discordServerId"]) + put("hostname", document["hostname"]) + put("queryPort", document["queryPort"]) + put("apiHostname", document["apiHostname"]) + put("apiPort", document["apiPort"]) + put("apiUsername", document["apiUsername"]) + put("apiPassword", document["apiPassword"]) + + if (document["playerActivityDiscordChannelId"] != null) { + put( + "playerActivityFeed", + mapOf( + "status" to Status.ACTIVE, + "discordChannelId" to document["playerActivityDiscordChannelId"] + ) + ) + } + + if (document["pvpKillFeedDiscordChannelId"] != null) { + put( + "pvpKillFeed", + mapOf( + "status" to Status.ACTIVE, + "discordChannelId" to document["pvpKillFeedDiscordChannelId"] + ) + ) + } + + if (document["embedEnabled"] == true) { + put( + "statusMonitor", + mapOf( + "status" to document["status"], + "discordChannelId" to document["discordChannelId"], + "displayServerDescription" to document["displayServerDescription"], + "displayPlayerGearLevel" to document["displayPlayerGearLevel"], + "currentEmbedMessageId" to document["currentEmbedMessageId"], + "currentFailedAttempts" to document["currentFailedAttempts"], + "currentFailedApiAttempts" to document["currentFailedApiAttempts"], + ) + ) + } + } + newCollection.insert(server) + } + oldCollection.remove(ObjectFilters.ALL) + } ) ) @@ -130,13 +197,14 @@ class DatabaseMigrationService( } // perform migration that affect the whole database - migrationsToPerform.forEach { migration -> migration.databaseAction(database) } + migrationsToPerform.forEach { migration -> + migration.databaseAction(database) - // perform migration that affect documents in the ServerStatusMonitor collection - val collection = database.getCollection(ObjectUtils.findObjectStoreName(ServerStatusMonitor::class.java)) - collection.find().forEach { document -> - migrationsToPerform.forEach { migration -> migration.documentAction(document) } - collection.update(document) + val collection = database.getCollection(migration.documentCollectionName) + collection.find().forEach { document -> + migration.documentAction(document) + collection.update(document) + } } repository.insert(Schema("V$currentAppVersion")) diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/OutdatedServerException.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/OutdatedServerException.kt new file mode 100644 index 0000000..3f2b128 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/OutdatedServerException.kt @@ -0,0 +1,5 @@ +package de.darkatra.vrising.discord.persistence + +import de.darkatra.vrising.discord.BotException + +class OutdatedServerException(message: String) : BotException(message) diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/ServerStatusMonitorRepository.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/ServerRepository.kt similarity index 55% rename from src/main/kotlin/de/darkatra/vrising/discord/persistence/ServerStatusMonitorRepository.kt rename to src/main/kotlin/de/darkatra/vrising/discord/persistence/ServerRepository.kt index 62be134..6b1f13e 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/persistence/ServerStatusMonitorRepository.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/ServerRepository.kt @@ -1,9 +1,7 @@ package de.darkatra.vrising.discord.persistence -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitorStatus +import de.darkatra.vrising.discord.persistence.model.Server import de.darkatra.vrising.discord.plus -import de.darkatra.vrising.discord.serverstatus.exceptions.OutdatedServerStatusMonitorException import org.dizitart.kno2.filters.and import org.dizitart.no2.FindOptions import org.dizitart.no2.Nitrite @@ -13,38 +11,39 @@ import org.springframework.stereotype.Service import java.time.Instant @Service -class ServerStatusMonitorRepository( +class ServerRepository( database: Nitrite, ) { - private var repository = database.getRepository(ServerStatusMonitor::class.java) + private val repository = database.getRepository(Server::class.java) - fun addServerStatusMonitor(serverStatusMonitor: ServerStatusMonitor) { + fun addServer(server: Server) { - if (repository.find(ObjectFilters.eq("id", serverStatusMonitor.id)).any()) { - throw IllegalStateException("Monitor with id '${serverStatusMonitor.id}' already exists.") + if (repository.find(ObjectFilters.eq("id", server.id)).any()) { + throw IllegalStateException("Server with id '${server.id}' already exists.") } - repository.insert(updateVersion(serverStatusMonitor)) + repository.insert(updateVersion(server)) } - fun updateServerStatusMonitor(serverStatusMonitor: ServerStatusMonitor) { + fun updateServer(server: Server) { @Suppress("DEPRECATION") // this is the internal usage the warning is referring to - val newVersion = serverStatusMonitor.version + val newVersion = server.version @Suppress("DEPRECATION") // this is the internal usage the warning is referring to - val databaseVersion = (repository.find(ObjectFilters.eq("id", serverStatusMonitor.id)).firstOrNull() - ?: throw OutdatedServerStatusMonitorException("Monitor with id '${serverStatusMonitor.id}' not found.")) + val databaseVersion = (repository.find(ObjectFilters.eq("id", server.id)).firstOrNull() + ?: throw OutdatedServerException("Server with id '${server.id}' not found.")) .version!! if (newVersion == null || databaseVersion > newVersion) { - throw OutdatedServerStatusMonitorException("Monitor with id '${serverStatusMonitor.id}' was already updated by another thread.") + throw OutdatedServerException("Server with id '${server.id}' was already updated by another thread.") } - repository.update(updateVersion(serverStatusMonitor)) + repository.update(updateVersion(server)) } - fun removeServerStatusMonitor(id: String, discordServerId: String? = null): Boolean { + fun removeServer(id: String, discordServerId: String? = null): Boolean { + var objectFilter: ObjectFilter = ObjectFilters.eq("id", id) if (discordServerId != null) { @@ -54,7 +53,7 @@ class ServerStatusMonitorRepository( return repository.remove(objectFilter).affectedCount > 0 } - fun getServerStatusMonitor(id: String, discordServerId: String? = null): ServerStatusMonitor? { + fun getServer(id: String, discordServerId: String? = null): Server? { var objectFilter: ObjectFilter = ObjectFilters.eq("id", id) @@ -62,23 +61,17 @@ class ServerStatusMonitorRepository( objectFilter += ObjectFilters.eq("discordServerId", discordServerId) } - return repository.find(objectFilter).firstOrNull() + return repository.find(objectFilter).firstOrNull().also { server -> + server?.linkServerAwareFields() + } } - fun getServerStatusMonitors( - discordServerId: String? = null, - status: ServerStatusMonitorStatus? = null, - offset: Int? = null, - limit: Int? = null - ): List { + fun getServers(discordServerId: String? = null, offset: Int? = null, limit: Int? = null): List { val objectFilter = buildList { if (discordServerId != null) { add(ObjectFilters.eq("discordServerId", discordServerId)) } - if (status != null) { - add(ObjectFilters.eq("status", status)) - } }.reduceOrNull { acc: ObjectFilter, objectFilter: ObjectFilter -> acc.and(objectFilter) } if (offset != null && limit != null) { @@ -97,12 +90,12 @@ class ServerStatusMonitorRepository( return when { objectFilter != null -> repository.find(objectFilter).toList() else -> repository.find().toList() + }.onEach { server -> + server.linkServerAwareFields() } } - fun count( - discordServerId: String? = null, - ): Int { + fun count(discordServerId: String? = null): Int { return when { discordServerId != null -> repository.find(ObjectFilters.eq("discordServerId", discordServerId)).size() @@ -110,8 +103,9 @@ class ServerStatusMonitorRepository( } } - private fun updateVersion(serverStatusMonitor: ServerStatusMonitor): ServerStatusMonitor { - return serverStatusMonitor.apply { + private fun updateVersion(server: Server): Server { + + return server.apply { @Suppress("DEPRECATION") // this is the internal usage the warning is referring to version = Instant.now().toEpochMilli() } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ErrorAware.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ErrorAware.kt new file mode 100644 index 0000000..9d29f3f --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ErrorAware.kt @@ -0,0 +1,22 @@ +package de.darkatra.vrising.discord.persistence.model + +import java.time.Instant + +interface ErrorAware { + + var recentErrors: List + + fun addError(throwable: Throwable, maxErrorsToKeep: Int) { + recentErrors = recentErrors + .takeLast((maxErrorsToKeep - 1).coerceAtLeast(0)) + .toMutableList() + .apply { + add( + Error( + message = "${throwable::class.simpleName}: ${throwable.message}", + timestamp = Instant.now().epochSecond + ) + ) + } + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/Leaderboard.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/Leaderboard.kt new file mode 100644 index 0000000..f12fa39 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/Leaderboard.kt @@ -0,0 +1,20 @@ +package de.darkatra.vrising.discord.persistence.model + +data class Leaderboard( + override var status: Status, + @Transient + private var server: Server? = null, + + // TODO: define the type of the leaderboard and think about other properties that should be configurable + + override var recentErrors: List = emptyList() +) : ErrorAware, ServerAware, StatusAware { + + override fun getServer(): Server { + return server!! + } + + override fun setServer(server: Server) { + this.server = server + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/PlayerActivityFeed.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/PlayerActivityFeed.kt new file mode 100644 index 0000000..979ced2 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/PlayerActivityFeed.kt @@ -0,0 +1,31 @@ +package de.darkatra.vrising.discord.persistence.model + +import java.time.Instant + +data class PlayerActivityFeed( + override var status: Status, + @Transient + private var server: Server? = null, + + var discordChannelId: String, + var lastUpdated: Long? = null, + + var currentFailedAttempts: Int = 0, + + override var recentErrors: List = emptyList() +) : ErrorAware, ServerAware, StatusAware { + + fun getLastUpdated(): Instant { + val lastUpdated = lastUpdated ?: return getServer().lastUpdated + return Instant.ofEpochMilli(lastUpdated) + } + + override fun getServer(): Server { + return server!! + } + + override fun setServer(server: Server) { + this.server = server + } +} + diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/PvpKillFeed.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/PvpKillFeed.kt new file mode 100644 index 0000000..1717ce3 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/PvpKillFeed.kt @@ -0,0 +1,30 @@ +package de.darkatra.vrising.discord.persistence.model + +import java.time.Instant + +data class PvpKillFeed( + override var status: Status, + @Transient + private var server: Server? = null, + + var discordChannelId: String, + var lastUpdated: Long? = null, + + var currentFailedAttempts: Int = 0, + + override var recentErrors: List = emptyList() +) : ErrorAware, ServerAware, StatusAware { + + fun getLastUpdated(): Instant { + val lastUpdated = lastUpdated ?: return getServer().lastUpdated + return Instant.ofEpochMilli(lastUpdated) + } + + override fun getServer(): Server { + return server!! + } + + override fun setServer(server: Server) { + this.server = server + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/Server.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/Server.kt new file mode 100644 index 0000000..8b1508b --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/Server.kt @@ -0,0 +1,80 @@ +package de.darkatra.vrising.discord.persistence.model + +import org.dizitart.no2.IndexType +import org.dizitart.no2.objects.Id +import org.dizitart.no2.objects.Index +import org.dizitart.no2.objects.Indices +import org.springframework.http.client.ClientHttpRequestInterceptor +import org.springframework.http.client.support.BasicAuthenticationInterceptor +import java.time.Instant + +@Indices( + value = [ + Index(value = "discordServerId", type = IndexType.NonUnique), + ] +) +data class Server( + @Id + val id: String, + @Deprecated("This field is updated automatically by the ServerRepository, manually update with caution") + var version: Long? = null, + + var discordServerId: String, + + var hostname: String, + var queryPort: Int, + + var apiHostname: String? = null, + var apiPort: Int? = null, + var apiUsername: String? = null, + var apiPassword: String? = null, + + var playerActivityFeed: PlayerActivityFeed? = null, + var pvpKillFeed: PvpKillFeed? = null, + var statusMonitor: StatusMonitor? = null, + + // TODO: decide on the way to store leaderboards + // option 1: store all leaderboards as a List and create commands that allow CRUD operations on that list + // this allows users to create more than one leaderboard per "type" (which might not be good - idk) + // option 2: store leaderboards in specific fields, such as `pvpLeaderboard` or `soulShardLeaderboard` + // this prevents users from having more than one leaderboard per type and probably also simplifies the commands + // a little bit by not introducing another id + var pvpLeaderboard: Leaderboard? = null +) : StatusAware { + + val apiEnabled: Boolean + get() = apiHostname != null && apiPort != null + + @Suppress("DEPRECATION") // this is the internal usage the warning is referring to + val lastUpdated: Instant + get() = Instant.ofEpochMilli(version!!) + + override val status: Status + get() { + return when (playerActivityFeed?.status == Status.ACTIVE + || statusMonitor?.status == Status.ACTIVE + || pvpKillFeed?.status == Status.ACTIVE + || pvpLeaderboard?.status == Status.ACTIVE) { + true -> Status.ACTIVE + false -> Status.INACTIVE + } + } + + fun linkServerAwareFields() { + playerActivityFeed?.setServer(this) + pvpKillFeed?.setServer(this) + statusMonitor?.setServer(this) + pvpLeaderboard?.setServer(this) + } + + fun getApiInterceptors(): List { + + val apiUsername = apiUsername + val apiPassword = apiPassword + + return when (apiUsername != null && apiPassword != null) { + true -> listOf(BasicAuthenticationInterceptor(apiUsername, apiPassword)) + false -> emptyList() + } + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ServerAware.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ServerAware.kt new file mode 100644 index 0000000..84b5aa0 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ServerAware.kt @@ -0,0 +1,6 @@ +package de.darkatra.vrising.discord.persistence.model + +interface ServerAware { + fun getServer(): Server + fun setServer(server: Server) +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ServerStatusMonitor.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ServerStatusMonitor.kt deleted file mode 100644 index 425ed59..0000000 --- a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ServerStatusMonitor.kt +++ /dev/null @@ -1,67 +0,0 @@ -package de.darkatra.vrising.discord.persistence.model - -import org.dizitart.no2.IndexType -import org.dizitart.no2.objects.Id -import org.dizitart.no2.objects.Index -import org.dizitart.no2.objects.Indices -import java.time.Instant - -@Indices( - value = [ - Index(value = "discordServerId", type = IndexType.NonUnique), - Index(value = "status", type = IndexType.NonUnique) - ] -) -data class ServerStatusMonitor( - @Id - val id: String, - @Deprecated("This field is updated automatically by the ServerStatusMonitorRepository, manually update with caution") - var version: Long? = null, - - var discordServerId: String, - var discordChannelId: String, - var playerActivityDiscordChannelId: String? = null, - var pvpKillFeedDiscordChannelId: String? = null, - - var hostname: String, - var queryPort: Int, - - var apiHostname: String? = null, - var apiPort: Int? = null, - var apiUsername: String? = null, - var apiPassword: String? = null, - - var status: ServerStatusMonitorStatus, - - var embedEnabled: Boolean = true, - var displayServerDescription: Boolean, - var displayPlayerGearLevel: Boolean, - - var currentEmbedMessageId: String? = null, - var currentFailedAttempts: Int = 0, - var currentFailedApiAttempts: Int = 0, - - var recentErrors: List = emptyList() -) { - - val apiEnabled: Boolean - get() = apiHostname != null && apiPort != null - - @Suppress("DEPRECATION") // this is the internal usage the warning is referring to - val lastUpdated: Instant - get() = Instant.ofEpochMilli(version!!) - - fun addError(throwable: Throwable, maxErrorsToKeep: Int) { - recentErrors = recentErrors - .takeLast((maxErrorsToKeep - 1).coerceAtLeast(0)) - .toMutableList() - .apply { - add( - Error( - message = "${throwable::class.simpleName}: ${throwable.message}", - timestamp = Instant.now().epochSecond - ) - ) - } - } -} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ServerStatusMonitorStatus.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/Status.kt similarity index 67% rename from src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ServerStatusMonitorStatus.kt rename to src/main/kotlin/de/darkatra/vrising/discord/persistence/model/Status.kt index 56e1a06..10e16c5 100644 --- a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/ServerStatusMonitorStatus.kt +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/Status.kt @@ -1,6 +1,6 @@ package de.darkatra.vrising.discord.persistence.model -enum class ServerStatusMonitorStatus { +enum class Status { INACTIVE, ACTIVE } diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/StatusAware.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/StatusAware.kt new file mode 100644 index 0000000..05a45e7 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/StatusAware.kt @@ -0,0 +1,17 @@ +package de.darkatra.vrising.discord.persistence.model + +interface StatusAware { + val status: Status +} + +fun Iterable.filterActive(): List { + return filter { + it.status == Status.ACTIVE + } +} + +fun Iterable.filterInactive(): List { + return filter { + it.status == Status.INACTIVE + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/StatusMonitor.kt b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/StatusMonitor.kt new file mode 100644 index 0000000..b9f5604 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/persistence/model/StatusMonitor.kt @@ -0,0 +1,27 @@ +package de.darkatra.vrising.discord.persistence.model + +data class StatusMonitor( + override var status: Status, + @Transient + private var server: Server? = null, + + var discordChannelId: String, + + var displayServerDescription: Boolean, + var displayPlayerGearLevel: Boolean, + + var currentEmbedMessageId: String? = null, + var currentFailedAttempts: Int = 0, + var currentFailedApiAttempts: Int = 0, + + override var recentErrors: List = emptyList() +) : ErrorAware, ServerAware, StatusAware { + + override fun getServer(): Server { + return server!! + } + + override fun setServer(server: Server) { + this.server = server + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/PlayerActivityFeedService.kt b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/PlayerActivityFeedService.kt new file mode 100644 index 0000000..5cdf2ad --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/PlayerActivityFeedService.kt @@ -0,0 +1,83 @@ +package de.darkatra.vrising.discord.serverstatus + +import de.darkatra.vrising.discord.BotProperties +import de.darkatra.vrising.discord.clients.botcompanion.BotCompanionClient +import de.darkatra.vrising.discord.clients.botcompanion.model.PlayerActivity +import de.darkatra.vrising.discord.getDiscordChannel +import de.darkatra.vrising.discord.persistence.model.PlayerActivityFeed +import de.darkatra.vrising.discord.persistence.model.Status +import de.darkatra.vrising.discord.tryCreateMessage +import dev.kord.core.Kord +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.Instant + +@Service +class PlayerActivityFeedService( + private val botProperties: BotProperties, + private val botCompanionClient: BotCompanionClient +) { + + private val logger = LoggerFactory.getLogger(javaClass) + + suspend fun updatePlayerActivityFeed(kord: Kord, playerActivityFeed: PlayerActivityFeed) { + + if (!playerActivityFeed.getServer().apiEnabled) { + logger.debug("Skipping player activity feed update for server '${playerActivityFeed.getServer().id}' because apiEnabled is false.") + return + } + + val playerActivityChannel = kord.getDiscordChannel(playerActivityFeed.discordChannelId).getOrElse { + logger.debug("Disabling player activity feed for server '${playerActivityFeed.getServer().id}' because the channel '${playerActivityFeed.discordChannelId}' does not seem to exist.") + playerActivityFeed.status = Status.INACTIVE + return + } + + val playerActivities = botCompanionClient.getPlayerActivities( + playerActivityFeed.getServer().apiHostname!!, + playerActivityFeed.getServer().apiPort!!, + playerActivityFeed.getServer().getApiInterceptors() + ).getOrElse { e -> + + logger.error("Exception updating the player activity feed for server '${playerActivityFeed.getServer().id}'", e) + playerActivityFeed.currentFailedAttempts += 1 + + if (botProperties.maxRecentErrors > 0) { + playerActivityFeed.addError(e, botProperties.maxRecentErrors) + } + + if (botProperties.maxFailedApiAttempts != 0 && playerActivityFeed.currentFailedAttempts >= botProperties.maxFailedApiAttempts) { + logger.warn("Disabling the player activity feed for server '${playerActivityFeed.getServer().id}' because it exceeded the max failed api attempts.") + playerActivityFeed.status = Status.INACTIVE + + // FIXME: mention the correct command to re-enable the player activity feed + playerActivityChannel.tryCreateMessage( + """Disabled the player activity feed for server '${playerActivityFeed.getServer().id}' because + |the bot companion did not respond successfully after ${botProperties.maxFailedApiAttempts} attempts. + |Please make sure the server-api-hostname and server-api-port are correct. + |You can re-enable the functionality using the update-server command.""".trimMargin() + ) + } + return + } + + playerActivityFeed.currentFailedAttempts = 0 + + playerActivities + .filter { playerActivity -> playerActivity.occurred.isAfter(playerActivityFeed.getLastUpdated()) } + .sortedWith(Comparator.comparing(PlayerActivity::occurred)) + .forEach { playerActivity -> + val action = when (playerActivity.type) { + PlayerActivity.Type.CONNECTED -> "joined" + PlayerActivity.Type.DISCONNECTED -> "left" + } + playerActivityChannel.tryCreateMessage( + ": ${playerActivity.playerName} $action the server." + ) + } + + playerActivityFeed.lastUpdated = Instant.now().toEpochMilli() + + logger.debug("Successfully updated the player activity feed for server '${playerActivityFeed.getServer().id}'.") + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/PvpKillFeedService.kt b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/PvpKillFeedService.kt new file mode 100644 index 0000000..7267516 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/PvpKillFeedService.kt @@ -0,0 +1,79 @@ +package de.darkatra.vrising.discord.serverstatus + +import de.darkatra.vrising.discord.BotProperties +import de.darkatra.vrising.discord.clients.botcompanion.BotCompanionClient +import de.darkatra.vrising.discord.clients.botcompanion.model.PvpKill +import de.darkatra.vrising.discord.getDiscordChannel +import de.darkatra.vrising.discord.persistence.model.PvpKillFeed +import de.darkatra.vrising.discord.persistence.model.Status +import de.darkatra.vrising.discord.tryCreateMessage +import dev.kord.core.Kord +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.Instant + +@Service +class PvpKillFeedService( + private val botProperties: BotProperties, + private val botCompanionClient: BotCompanionClient +) { + + private val logger = LoggerFactory.getLogger(javaClass) + + suspend fun updatePvpKillFeed(kord: Kord, pvpKillFeed: PvpKillFeed) { + + if (!pvpKillFeed.getServer().apiEnabled) { + logger.debug("Skipping pvp kill feed update for server '${pvpKillFeed.getServer().id}' because apiEnabled is false.") + return + } + + val pvpKillFeedChannel = kord.getDiscordChannel(pvpKillFeed.discordChannelId).getOrElse { + logger.debug("Disabling pvp kill feed for server '${pvpKillFeed.getServer().id}' because the channel '${pvpKillFeed.discordChannelId}' does not seem to exist.") + pvpKillFeed.status = Status.INACTIVE + return + } + + val pvpKills = botCompanionClient.getPvpKills( + pvpKillFeed.getServer().apiHostname!!, + pvpKillFeed.getServer().apiPort!!, + pvpKillFeed.getServer().getApiInterceptors() + ).getOrElse { e -> + + logger.error("Exception updating the pvp kill feed for server ${pvpKillFeed.getServer().id}", e) + pvpKillFeed.currentFailedAttempts += 1 + + if (botProperties.maxRecentErrors > 0) { + pvpKillFeed.addError(e, botProperties.maxRecentErrors) + } + + if (botProperties.maxFailedApiAttempts != 0 && pvpKillFeed.currentFailedAttempts >= botProperties.maxFailedApiAttempts) { + logger.warn("Disabling the pvp kill feed for server '${pvpKillFeed.getServer().id}' because it exceeded the max failed api attempts.") + pvpKillFeed.status = Status.INACTIVE + + // FIXME: mention the correct command to re-enable the player activity feed + pvpKillFeedChannel.tryCreateMessage( + """Disabled the pvp kill feed for server '${pvpKillFeed.getServer().id}' because + |the bot companion did not respond successfully after ${botProperties.maxFailedApiAttempts} attempts. + |Please make sure the server-api-hostname and server-api-port are correct. + |You can re-enable the functionality using the update-server command.""".trimMargin() + ) + } + return + } + + pvpKillFeed.currentFailedAttempts = 0 + + pvpKills + .filter { pvpKill -> pvpKill.occurred.isAfter(pvpKillFeed.getLastUpdated()) } + .sortedWith(Comparator.comparing(PvpKill::occurred)) + .forEach { pvpKill -> + pvpKillFeedChannel.tryCreateMessage( + ": ${pvpKill.killer.name} (${pvpKill.killer.gearLevel}) killed ${pvpKill.victim.name} (${pvpKill.victim.gearLevel})." + ) + } + + pvpKillFeed.lastUpdated = Instant.now().toEpochMilli() + + logger.debug("Successfully updated the pvp kill feed for server '${pvpKillFeed.getServer().id}'.") + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/ServerService.kt b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/ServerService.kt new file mode 100644 index 0000000..8e8ccb2 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/ServerService.kt @@ -0,0 +1,97 @@ +package de.darkatra.vrising.discord.serverstatus + +import de.darkatra.vrising.discord.persistence.OutdatedServerException +import de.darkatra.vrising.discord.persistence.ServerRepository +import de.darkatra.vrising.discord.persistence.model.Server +import de.darkatra.vrising.discord.persistence.model.Status +import de.darkatra.vrising.discord.persistence.model.filterActive +import de.darkatra.vrising.discord.persistence.model.filterInactive +import dev.kord.core.Kord +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.stereotype.Service +import java.time.Instant +import java.time.temporal.ChronoUnit + +@Service +class ServerService( + private val serverRepository: ServerRepository, + private val statusMonitorService: StatusMonitorService, + private val playerActivityFeedService: PlayerActivityFeedService, + private val pvpKillFeedService: PvpKillFeedService +) { + + private val logger = LoggerFactory.getLogger(javaClass) + + suspend fun updateServers(kord: Kord) { + + val activeServers = serverRepository.getServers().filterActive() + activeServers.forEach { server -> + + MDC.put("server-id", server.id) + + updateStatusMonitor(kord, server) + updatePlayerActivityFeed(kord, server) + updatePvpKillFeed(kord, server) + + try { + serverRepository.updateServer(server) + } catch (e: OutdatedServerException) { + logger.debug("Server was updated or deleted by another thread. Will ignore this exception and proceed with the next server.", e) + } + + MDC.clear() + } + } + + suspend fun cleanupInactiveServers(kord: Kord) { + + val inactiveServers = serverRepository.getServers().filterInactive() + if (inactiveServers.isEmpty()) { + logger.info("No inactive servers to clean up.") + return + } + + val sevenDaysAgo = Instant.now().minus(7, ChronoUnit.DAYS) + inactiveServers.forEach { server -> + if (server.lastUpdated.isBefore(sevenDaysAgo)) { + serverRepository.removeServer(server.id) + } + } + + logger.info("Successfully removed ${inactiveServers.count()} servers with no active feature.") + } + + private suspend fun updateStatusMonitor(kord: Kord, server: Server) { + + val statusMonitor = server.statusMonitor + if (statusMonitor == null || statusMonitor.status == Status.INACTIVE) { + logger.debug("No active status monitor to update for server '${server.id}'.") + return + } + + statusMonitorService.updateStatusMonitor(kord, statusMonitor) + } + + private suspend fun updatePlayerActivityFeed(kord: Kord, server: Server) { + + val playerActivityFeed = server.playerActivityFeed + if (playerActivityFeed == null || playerActivityFeed.status == Status.INACTIVE) { + logger.debug("No active player activity feed to update for server '${server.id}'.") + return + } + + playerActivityFeedService.updatePlayerActivityFeed(kord, playerActivityFeed) + } + + private suspend fun updatePvpKillFeed(kord: Kord, server: Server) { + + val pvpKillFeed = server.pvpKillFeed + if (pvpKillFeed == null || pvpKillFeed.status == Status.INACTIVE) { + logger.debug("No active pvp kill feed to update for server '${server.id}'.") + return + } + + pvpKillFeedService.updatePvpKillFeed(kord, pvpKillFeed) + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/ServerStatusMonitorService.kt b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/ServerStatusMonitorService.kt deleted file mode 100644 index 0f0207c..0000000 --- a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/ServerStatusMonitorService.kt +++ /dev/null @@ -1,324 +0,0 @@ -package de.darkatra.vrising.discord.serverstatus - -import de.darkatra.vrising.discord.BotProperties -import de.darkatra.vrising.discord.clients.botcompanion.BotCompanionClient -import de.darkatra.vrising.discord.clients.botcompanion.model.PlayerActivity -import de.darkatra.vrising.discord.clients.botcompanion.model.PvpKill -import de.darkatra.vrising.discord.clients.serverquery.ServerQueryClient -import de.darkatra.vrising.discord.getDiscordChannel -import de.darkatra.vrising.discord.persistence.ServerStatusMonitorRepository -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitorStatus -import de.darkatra.vrising.discord.serverstatus.exceptions.OutdatedServerStatusMonitorException -import de.darkatra.vrising.discord.serverstatus.model.ServerInfo -import dev.kord.common.entity.Snowflake -import dev.kord.core.Kord -import dev.kord.core.behavior.channel.createEmbed -import dev.kord.core.behavior.edit -import dev.kord.core.exception.EntityNotFoundException -import dev.kord.rest.builder.message.EmbedBuilder -import dev.kord.rest.builder.message.embed -import org.slf4j.LoggerFactory -import org.slf4j.MDC -import org.springframework.http.client.ClientHttpRequestInterceptor -import org.springframework.http.client.support.BasicAuthenticationInterceptor -import org.springframework.stereotype.Service -import java.time.Instant -import java.time.temporal.ChronoUnit - -@Service -class ServerStatusMonitorService( - private val serverStatusMonitorRepository: ServerStatusMonitorRepository, - private val serverQueryClient: ServerQueryClient, - private val botCompanionClient: BotCompanionClient, - private val botProperties: BotProperties -) { - - private val logger = LoggerFactory.getLogger(javaClass) - - suspend fun updateServerStatusMonitors(kord: Kord) { - - serverStatusMonitorRepository.getServerStatusMonitors(status = ServerStatusMonitorStatus.ACTIVE).forEach { serverStatusMonitor -> - - MDC.put("server-status-monitor-id", serverStatusMonitor.id) - - updateServerStatusMonitor(kord, serverStatusMonitor) - updatePlayerActivityFeed(kord, serverStatusMonitor) - updatePvpKillFeed(kord, serverStatusMonitor) - try { - serverStatusMonitorRepository.updateServerStatusMonitor(serverStatusMonitor) - } catch (e: OutdatedServerStatusMonitorException) { - logger.debug("Server status monitor was updated or deleted by another thread. Will ignore this exception and proceed as usual.", e) - } - - MDC.clear() - } - } - - suspend fun cleanupInactiveServerStatusMonitors(kord: Kord) { - - val sevenDaysAgo = Instant.now().minus(7, ChronoUnit.DAYS) - val inactiveServerStatusMonitors = serverStatusMonitorRepository.getServerStatusMonitors(status = ServerStatusMonitorStatus.INACTIVE) - inactiveServerStatusMonitors.forEach { serverStatusMonitor -> - if (serverStatusMonitor.lastUpdated.isBefore(sevenDaysAgo)) { - serverStatusMonitorRepository.removeServerStatusMonitor(serverStatusMonitor.id) - } - } - - logger.info("Successfully removed ${inactiveServerStatusMonitors.count()} inactive server status monitors.") - } - - private suspend fun updateServerStatusMonitor(kord: Kord, serverStatusMonitor: ServerStatusMonitor) { - - if (!serverStatusMonitor.embedEnabled) { - logger.debug("Skipping server monitor '${serverStatusMonitor.id}' because embedEnabled is false.") - return - } - - val channel = kord.getDiscordChannel(serverStatusMonitor.discordChannelId).getOrElse { - logger.debug("Disabling server monitor '${serverStatusMonitor.id}' because the channel '${serverStatusMonitor.discordChannelId}' does not seem to exist.") - serverStatusMonitor.status = ServerStatusMonitorStatus.INACTIVE - return - } - - val serverInfo = serverQueryClient.getServerStatus( - serverStatusMonitor.hostname, - serverStatusMonitor.queryPort - ).map { serverStatus -> - ServerInfo.of(serverStatus) - }.getOrElse { e -> - - logger.error("Exception fetching the status of '${serverStatusMonitor.id}'.", e) - serverStatusMonitor.currentFailedAttempts += 1 - - if (botProperties.maxRecentErrors > 0) { - serverStatusMonitor.addError(e, botProperties.maxRecentErrors) - } - - try { - - if (serverStatusMonitor.currentEmbedMessageId == null && serverStatusMonitor.currentFailedAttempts == 1) { - channel.createMessage( - """The status check for your status monitor '${serverStatusMonitor.id}' failed. - |Please check the detailed error message using the get-server-details command.""".trimMargin() - ) - } - - if (botProperties.maxFailedAttempts != 0 && serverStatusMonitor.currentFailedAttempts >= botProperties.maxFailedAttempts) { - logger.warn("Disabling server monitor '${serverStatusMonitor.id}' because it exceeded the max failed attempts.") - serverStatusMonitor.status = ServerStatusMonitorStatus.INACTIVE - - channel.createMessage( - """Disabled server status monitor '${serverStatusMonitor.id}' because the server did not - |respond successfully after ${botProperties.maxFailedAttempts} attempts. - |Please make sure the server is running and is accessible from the internet to use this bot. - |You can re-enable the server status monitor using the update-server command.""".trimMargin() - ) - } - } catch (e: Exception) { - logger.warn("Could not post status message for monitor '${serverStatusMonitor.id}'.", e) - } - return - } - - if (serverStatusMonitor.apiEnabled && serverStatusMonitor.displayPlayerGearLevel) { - - val characters = botCompanionClient.getCharacters( - serverStatusMonitor.apiHostname!!, - serverStatusMonitor.apiPort!!, - getInterceptors(serverStatusMonitor) - ).getOrElse { e -> - - logger.warn("Could not resolve characters for server monitor '${serverStatusMonitor.id}'. Player Gear level will not be displayed.", e) - serverStatusMonitor.currentFailedApiAttempts += 1 - - if (botProperties.maxRecentErrors > 0) { - serverStatusMonitor.addError(e, botProperties.maxRecentErrors) - } - - try { - - if (botProperties.maxFailedApiAttempts != 0 && serverStatusMonitor.currentFailedApiAttempts >= botProperties.maxFailedApiAttempts) { - logger.warn("Disabling displayPlayerGearLevel for server monitor '${serverStatusMonitor.id}' because it exceeded the max failed api attempts.") - serverStatusMonitor.displayPlayerGearLevel = false - - channel.createMessage( - """Disabled displayPlayerGearLevel for server status monitor '${serverStatusMonitor.id}' because - |the bot companion did not respond successfully after ${botProperties.maxFailedApiAttempts} attempts. - |Please make sure the server-api-hostname and server-api-port are correct. - |You can re-enable the functionality using the update-server command.""".trimMargin() - ) - } - } catch (e: Exception) { - logger.warn("Could not post status message for monitor '${serverStatusMonitor.id}'", e) - } - return - } - - serverInfo.enrichCompanionData(characters) - serverStatusMonitor.currentFailedApiAttempts = 0 - } - - val embedCustomizer: (embedBuilder: EmbedBuilder) -> Unit = { embedBuilder -> - ServerStatusEmbed.buildEmbed( - serverInfo, - serverStatusMonitor.apiEnabled, - serverStatusMonitor.displayServerDescription, - serverStatusMonitor.displayPlayerGearLevel, - embedBuilder - ) - } - - val currentEmbedMessageId = serverStatusMonitor.currentEmbedMessageId - if (currentEmbedMessageId != null) { - try { - channel.getMessage(Snowflake(currentEmbedMessageId)) - .edit { embed(embedCustomizer) } - - serverStatusMonitor.currentFailedAttempts = 0 - - logger.debug("Successfully updated the status of server monitor '${serverStatusMonitor.id}'.") - return - } catch (e: EntityNotFoundException) { - serverStatusMonitor.currentEmbedMessageId = null - } - } - - serverStatusMonitor.currentEmbedMessageId = channel.createEmbed(embedCustomizer).id.toString() - serverStatusMonitor.currentFailedAttempts = 0 - - logger.debug("Successfully updated the status and persisted the embedId of server monitor '${serverStatusMonitor.id}'.") - } - - private suspend fun updatePlayerActivityFeed(kord: Kord, serverStatusMonitor: ServerStatusMonitor) { - - if (!serverStatusMonitor.apiEnabled) { - logger.debug("Skipping player activity feed update for server monitor '${serverStatusMonitor.id}' because apiEnabled is false.") - return - } - - val playerActivityDiscordChannelId = serverStatusMonitor.playerActivityDiscordChannelId ?: return - val playerActivityChannel = kord.getDiscordChannel(playerActivityDiscordChannelId).getOrElse { - logger.debug("Disabling player activity feed for server monitor '${serverStatusMonitor.id}' because the channel '${playerActivityDiscordChannelId}' does not seem to exist.") - serverStatusMonitor.playerActivityDiscordChannelId = null - return - } - - val playerActivities = botCompanionClient.getPlayerActivities( - serverStatusMonitor.apiHostname!!, - serverStatusMonitor.apiPort!!, - getInterceptors(serverStatusMonitor) - ).getOrElse { e -> - - logger.error("Exception updating the player activity feed of '${serverStatusMonitor.id}'", e) - serverStatusMonitor.currentFailedApiAttempts += 1 - - if (botProperties.maxRecentErrors > 0) { - serverStatusMonitor.addError(e, botProperties.maxRecentErrors) - } - - try { - if (botProperties.maxFailedApiAttempts != 0 && serverStatusMonitor.currentFailedApiAttempts >= botProperties.maxFailedApiAttempts) { - logger.warn("Disabling the player activity feed for server monitor '${serverStatusMonitor.id}' because it exceeded the max failed api attempts.") - serverStatusMonitor.playerActivityDiscordChannelId = null - - playerActivityChannel.createMessage( - """Disabled the player activity feed for server status monitor '${serverStatusMonitor.id}' because - |the bot companion did not respond successfully after ${botProperties.maxFailedApiAttempts} attempts. - |Please make sure the server-api-hostname and server-api-port are correct. - |You can re-enable the functionality using the update-server command.""".trimMargin() - ) - } - } catch (e: Exception) { - logger.warn("Could not post status message for monitor '${serverStatusMonitor.id}'", e) - } - return - } - - serverStatusMonitor.currentFailedApiAttempts = 0 - - playerActivities - .filter { playerActivity -> playerActivity.occurred.isAfter(serverStatusMonitor.lastUpdated) } - .sortedWith(Comparator.comparing(PlayerActivity::occurred)) - .forEach { playerActivity -> - val action = when (playerActivity.type) { - PlayerActivity.Type.CONNECTED -> "joined" - PlayerActivity.Type.DISCONNECTED -> "left" - } - playerActivityChannel.createMessage( - ": ${playerActivity.playerName} $action the server." - ) - } - - logger.debug("Successfully updated the player activity feed of server monitor '${serverStatusMonitor.id}'.") - } - - private suspend fun updatePvpKillFeed(kord: Kord, serverStatusMonitor: ServerStatusMonitor) { - - if (!serverStatusMonitor.apiEnabled) { - logger.debug("Skipping pvp kill feed update for server monitor '${serverStatusMonitor.id}' because apiEnabled is false.") - return - } - - val pvpKillFeedDiscordChannelId = serverStatusMonitor.pvpKillFeedDiscordChannelId ?: return - val pvpKillFeedChannel = kord.getDiscordChannel(pvpKillFeedDiscordChannelId).getOrElse { - logger.debug("Disabling pvp kill feed for server monitor '${serverStatusMonitor.id}' because the channel '${pvpKillFeedDiscordChannelId}' does not seem to exist.") - serverStatusMonitor.pvpKillFeedDiscordChannelId = null - return - } - - val pvpKills = botCompanionClient.getPvpKills( - serverStatusMonitor.apiHostname!!, - serverStatusMonitor.apiPort!!, - getInterceptors(serverStatusMonitor) - ).getOrElse { e -> - - logger.error("Exception updating the pvp kill feed of ${serverStatusMonitor.id}", e) - serverStatusMonitor.currentFailedApiAttempts += 1 - - if (botProperties.maxRecentErrors > 0) { - serverStatusMonitor.addError(e, botProperties.maxRecentErrors) - } - - try { - if (botProperties.maxFailedApiAttempts != 0 && serverStatusMonitor.currentFailedApiAttempts >= botProperties.maxFailedApiAttempts) { - logger.warn("Disabling the pvp kill feed for server monitor '${serverStatusMonitor.id}' because it exceeded the max failed api attempts.") - serverStatusMonitor.pvpKillFeedDiscordChannelId = null - - pvpKillFeedChannel.createMessage( - """Disabled the pvp kill feed for server status monitor '${serverStatusMonitor.id}' because - |the bot companion did not respond successfully after ${botProperties.maxFailedApiAttempts} attempts. - |Please make sure the server-api-hostname and server-api-port are correct. - |You can re-enable the functionality using the update-server command.""".trimMargin() - ) - } - } catch (e: Exception) { - logger.warn("Could not post status message for monitor '${serverStatusMonitor.id}'", e) - } - return - } - - serverStatusMonitor.currentFailedApiAttempts = 0 - - pvpKills - .filter { pvpKill -> pvpKill.occurred.isAfter(serverStatusMonitor.lastUpdated) } - .sortedWith(Comparator.comparing(PvpKill::occurred)) - .forEach { pvpKill -> - pvpKillFeedChannel.createMessage( - ": ${pvpKill.killer.name} (${pvpKill.killer.gearLevel}) killed ${pvpKill.victim.name} (${pvpKill.victim.gearLevel})." - ) - } - - logger.debug("Successfully updated the pvp kill feed of server monitor '${serverStatusMonitor.id}'.") - } - - private fun getInterceptors(serverStatusMonitor: ServerStatusMonitor): List { - - val (_, _, _, _, _, _, _, _, _, _, apiUsername, apiPassword, _, _, _, _, _, _) = serverStatusMonitor - - return when (apiUsername != null && apiPassword != null) { - true -> listOf(BasicAuthenticationInterceptor(apiUsername, apiPassword)) - false -> emptyList() - } - } -} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/StatusMonitorService.kt b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/StatusMonitorService.kt new file mode 100644 index 0000000..9a32058 --- /dev/null +++ b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/StatusMonitorService.kt @@ -0,0 +1,140 @@ +package de.darkatra.vrising.discord.serverstatus + +import de.darkatra.vrising.discord.BotProperties +import de.darkatra.vrising.discord.clients.botcompanion.BotCompanionClient +import de.darkatra.vrising.discord.clients.serverquery.ServerQueryClient +import de.darkatra.vrising.discord.getDiscordChannel +import de.darkatra.vrising.discord.persistence.model.Status +import de.darkatra.vrising.discord.persistence.model.StatusMonitor +import de.darkatra.vrising.discord.serverstatus.model.ServerInfo +import de.darkatra.vrising.discord.tryCreateMessage +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.behavior.channel.createEmbed +import dev.kord.core.behavior.edit +import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.rest.builder.message.embed +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service + +@Service +class StatusMonitorService( + private val botProperties: BotProperties, + private val serverQueryClient: ServerQueryClient, + private val botCompanionClient: BotCompanionClient +) { + + private val logger = LoggerFactory.getLogger(javaClass) + + suspend fun updateStatusMonitor(kord: Kord, statusMonitor: StatusMonitor) { + + val channel = kord.getDiscordChannel(statusMonitor.discordChannelId).getOrElse { + logger.debug("Disabling server monitor for server '${statusMonitor.getServer().id}' because the channel '${statusMonitor.discordChannelId}' does not seem to exist.") + statusMonitor.status = Status.INACTIVE + return + } + + val serverInfo = serverQueryClient.getServerStatus( + statusMonitor.getServer().hostname, + statusMonitor.getServer().queryPort + ).map { serverStatus -> + ServerInfo.of(serverStatus) + }.getOrElse { e -> + + logger.error("Exception updating the status monitor for server '${statusMonitor.getServer().id}'.", e) + statusMonitor.currentFailedAttempts += 1 + + if (botProperties.maxRecentErrors > 0) { + statusMonitor.addError(e, botProperties.maxRecentErrors) + } + + + if (statusMonitor.currentEmbedMessageId == null && statusMonitor.currentFailedAttempts == 1) { + // FIXME: mention the correct command to retrieve the error messages for the status monitor + channel.tryCreateMessage( + """Failed to update the status monitor for server '${statusMonitor.getServer().id}'. + |Please check the detailed error message using the get-server-details command.""".trimMargin() + ) + } + + if (botProperties.maxFailedAttempts != 0 && statusMonitor.currentFailedAttempts >= botProperties.maxFailedAttempts) { + logger.warn("Disabling server monitor for server '${statusMonitor.getServer().id}' because it exceeded the max failed attempts.") + statusMonitor.status = Status.INACTIVE + + // FIXME: mention the correct command to re-enable the status monitor + channel.tryCreateMessage( + """Disabled status monitor for server '${statusMonitor.getServer().id}' because the server did not + |respond successfully after ${botProperties.maxFailedAttempts} attempts. + |Please make sure the server is running and is accessible from the internet to use this bot. + |You can re-enable the server status monitor using the update-server command.""".trimMargin() + ) + } + return + } + + if (statusMonitor.getServer().apiEnabled && statusMonitor.displayPlayerGearLevel) { + + val characters = botCompanionClient.getCharacters( + statusMonitor.getServer().apiHostname!!, + statusMonitor.getServer().apiPort!!, + statusMonitor.getServer().getApiInterceptors() + ).getOrElse { e -> + + logger.warn("Could not resolve characters for status monitor for server '${statusMonitor.getServer().id}'.", e) + statusMonitor.currentFailedApiAttempts += 1 + + if (botProperties.maxRecentErrors > 0) { + statusMonitor.addError(e, botProperties.maxRecentErrors) + } + + + if (botProperties.maxFailedApiAttempts != 0 && statusMonitor.currentFailedApiAttempts >= botProperties.maxFailedApiAttempts) { + logger.warn("Disabling displayPlayerGearLevel for status monitor of server '${statusMonitor.getServer().id}' because it exceeded the max failed api attempts.") + statusMonitor.displayPlayerGearLevel = false + + // FIXME: mention the correct command to re-enable the status monitor + channel.tryCreateMessage( + """The status monitor for server '${statusMonitor.getServer().id}' will no longer display the players gear level because + |the bot companion did not respond successfully after ${botProperties.maxFailedApiAttempts} attempts. + |Please make sure the server-api-hostname and server-api-port are correct. + |You can re-enable the functionality using the update-server command.""".trimMargin() + ) + } + return + } + + serverInfo.enrichCompanionData(characters) + statusMonitor.currentFailedApiAttempts = 0 + } + + val embedCustomizer: (embedBuilder: EmbedBuilder) -> Unit = { embedBuilder -> + ServerStatusEmbed.buildEmbed( + serverInfo, + statusMonitor.getServer().apiEnabled, + statusMonitor.displayServerDescription, + statusMonitor.displayPlayerGearLevel, + embedBuilder + ) + } + + val currentEmbedMessageId = statusMonitor.currentEmbedMessageId + if (currentEmbedMessageId != null) { + try { + channel.getMessage(Snowflake(currentEmbedMessageId)) + .edit { embed(embedCustomizer) } + + statusMonitor.currentFailedAttempts = 0 + + logger.debug("Successfully updated the status monitor for server '${statusMonitor.getServer().id}'.") + return + } catch (e: Exception) { + statusMonitor.currentEmbedMessageId = null + } + } + + statusMonitor.currentEmbedMessageId = channel.createEmbed(embedCustomizer).id.toString() + statusMonitor.currentFailedAttempts = 0 + + logger.debug("Successfully updated the status and persisted the embedId for server monitor of server '${statusMonitor.getServer().id}'.") + } +} diff --git a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/exceptions/InvalidDiscordChannelException.kt b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/exceptions/InvalidDiscordChannelException.kt deleted file mode 100644 index 03cfd99..0000000 --- a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/exceptions/InvalidDiscordChannelException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package de.darkatra.vrising.discord.serverstatus.exceptions - -import de.darkatra.vrising.discord.BotException - -class InvalidDiscordChannelException(message: String, cause: Throwable? = null) : BotException(message, cause) diff --git a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/exceptions/OutdatedServerStatusMonitorException.kt b/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/exceptions/OutdatedServerStatusMonitorException.kt deleted file mode 100644 index ab53976..0000000 --- a/src/main/kotlin/de/darkatra/vrising/discord/serverstatus/exceptions/OutdatedServerStatusMonitorException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package de.darkatra.vrising.discord.serverstatus.exceptions - -import de.darkatra.vrising.discord.BotException - -class OutdatedServerStatusMonitorException(message: String) : BotException(message) diff --git a/src/test/kotlin/de/darkatra/vrising/discord/DatabaseConfigurationTestUtils.kt b/src/test/kotlin/de/darkatra/vrising/discord/DatabaseConfigurationTestUtils.kt index 9aa609e..c912393 100644 --- a/src/test/kotlin/de/darkatra/vrising/discord/DatabaseConfigurationTestUtils.kt +++ b/src/test/kotlin/de/darkatra/vrising/discord/DatabaseConfigurationTestUtils.kt @@ -7,14 +7,24 @@ import java.io.File object DatabaseConfigurationTestUtils { + val DATABASE_FILE_V1_2_x = File(DatabaseConfigurationTestUtils::class.java.getResource("/persistence/v1.2.db")!!.toURI()) + val DATABASE_FILE_V2_10_5 = File(DatabaseConfigurationTestUtils::class.java.getResource("/persistence/v2.10.5.db")!!.toURI()) private val logger = LoggerFactory.getLogger(javaClass) - fun getTestDatabase(): Nitrite { + fun getTestDatabase(fromTemplate: File? = null): Nitrite { + + val databaseFile = File.createTempFile("v-rising-bot", ".db").also { + logger.info("Test Db location: " + it.absolutePath) + } + + if (fromTemplate != null) { + logger.info("Loading template from '${fromTemplate.absolutePath}'.") + fromTemplate.copyTo(databaseFile, overwrite = true) + } + return Nitrite.builder() .compressed() - .filePath(File.createTempFile("v-rising-bot", ".db").also { - logger.info("Test Db location: " + it.absolutePath) - }) + .filePath(databaseFile) .openOrCreate() } diff --git a/src/test/kotlin/de/darkatra/vrising/discord/RuntimeHintsTest.kt b/src/test/kotlin/de/darkatra/vrising/discord/RuntimeHintsTest.kt index 9b607da..100e1c2 100644 --- a/src/test/kotlin/de/darkatra/vrising/discord/RuntimeHintsTest.kt +++ b/src/test/kotlin/de/darkatra/vrising/discord/RuntimeHintsTest.kt @@ -1,7 +1,17 @@ package de.darkatra.vrising.discord +import de.darkatra.vrising.discord.clients.botcompanion.model.Character +import de.darkatra.vrising.discord.clients.botcompanion.model.PlayerActivity +import de.darkatra.vrising.discord.clients.botcompanion.model.PvpKill +import de.darkatra.vrising.discord.clients.botcompanion.model.VBlood import de.darkatra.vrising.discord.migration.Schema -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor +import de.darkatra.vrising.discord.persistence.model.Error +import de.darkatra.vrising.discord.persistence.model.Leaderboard +import de.darkatra.vrising.discord.persistence.model.PlayerActivityFeed +import de.darkatra.vrising.discord.persistence.model.PvpKillFeed +import de.darkatra.vrising.discord.persistence.model.Server +import de.darkatra.vrising.discord.persistence.model.Status +import de.darkatra.vrising.discord.persistence.model.StatusMonitor import org.junit.jupiter.api.Test import org.springframework.aot.hint.annotation.RegisterReflectionForBinding import org.springframework.boot.test.context.SpringBootTest @@ -18,7 +28,27 @@ class RuntimeHintsTest { // workaround to generate runtime hints for unit tests } + // should use the same hints and reflection bindings as in Bot.kt @ImportRuntimeHints(BotRuntimeHints::class) - @RegisterReflectionForBinding(BotProperties::class, Schema::class, ServerStatusMonitor::class) + @RegisterReflectionForBinding( + // properties + BotProperties::class, + // database + Error::class, + Leaderboard::class, + Schema::class, + PlayerActivityFeed::class, + PvpKillFeed::class, + Server::class, + Status::class, + StatusMonitor::class, + // http + Character::class, + PlayerActivity::class, + PlayerActivity.Type::class, + PvpKill::class, + PvpKill.Player::class, + VBlood::class, + ) class TestConfiguration } diff --git a/src/test/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigrationServiceTest.kt b/src/test/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigrationServiceTest.kt index 29c6f76..607ee83 100644 --- a/src/test/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigrationServiceTest.kt +++ b/src/test/kotlin/de/darkatra/vrising/discord/migration/DatabaseMigrationServiceTest.kt @@ -1,101 +1,207 @@ package de.darkatra.vrising.discord.migration import de.darkatra.vrising.discord.DatabaseConfigurationTestUtils -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor +import de.darkatra.vrising.discord.persistence.model.Server +import de.darkatra.vrising.discord.persistence.model.Status import org.assertj.core.api.Assertions.assertThat import org.dizitart.no2.Document -import org.dizitart.no2.Nitrite -import org.dizitart.no2.util.ObjectUtils -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.condition.DisabledInNativeImage -@DisabledInNativeImage class DatabaseMigrationServiceTest { - private lateinit var database: Nitrite - - @BeforeEach - fun setUp() { - database = DatabaseConfigurationTestUtils.getTestDatabase() - } - @Test fun `should perform database migration when no schema was found`() { - val databaseMigrationService = DatabaseMigrationService( - database = database, - appVersionFromPom = "1.5.0" - ) + DatabaseConfigurationTestUtils.getTestDatabase().use { database -> - assertThat(databaseMigrationService.migrateToLatestVersion()).isTrue() + val databaseMigrationService = DatabaseMigrationService( + database = database, + appVersionFromPom = "1.5.0" + ) - val repository = database.getRepository(Schema::class.java) + assertThat(databaseMigrationService.migrateToLatestVersion()).isTrue() - val schemas = repository.find().toList() - assertThat(schemas).hasSize(1) - assertThat(schemas).first().extracting(Schema::appVersion).isEqualTo("V1.5.0") + val repository = database.getRepository(Schema::class.java) + + val schemas = repository.find().toList() + assertThat(schemas).hasSize(1) + assertThat(schemas).first().extracting(Schema::appVersion).isEqualTo("V1.5.0") + } } @Test fun `should not perform database migration when schema matches the current version`() { - val repository = database.getRepository(Schema::class.java) - repository.insert(Schema(appVersion = "V1.4.0")) - repository.insert(Schema(appVersion = "V1.5.0")) - repository.insert(Schema(appVersion = "V1.6.0")) - repository.insert(Schema(appVersion = "V1.8.0")) - repository.insert(Schema(appVersion = "V2.2.0")) - repository.insert(Schema(appVersion = "V2.3.0")) - repository.insert(Schema(appVersion = "V2.9.0")) - repository.insert(Schema(appVersion = "V2.10.0")) - repository.insert(Schema(appVersion = "V2.10.2")) - - val databaseMigrationService = DatabaseMigrationService( - database = database, - appVersionFromPom = "2.10.2" - ) - - assertThat(databaseMigrationService.migrateToLatestVersion()).isFalse() - - val schemas = repository.find().toList() - assertThat(schemas).hasSize(9) + DatabaseConfigurationTestUtils.getTestDatabase().use { database -> + + val repository = database.getRepository(Schema::class.java) + repository.insert(Schema(appVersion = "V1.4.0")) + repository.insert(Schema(appVersion = "V1.5.0")) + repository.insert(Schema(appVersion = "V1.6.0")) + repository.insert(Schema(appVersion = "V1.8.0")) + repository.insert(Schema(appVersion = "V2.2.0")) + repository.insert(Schema(appVersion = "V2.3.0")) + repository.insert(Schema(appVersion = "V2.9.0")) + repository.insert(Schema(appVersion = "V2.10.0")) + repository.insert(Schema(appVersion = "V2.10.2")) + repository.insert(Schema(appVersion = "V2.11.0")) + + val databaseMigrationService = DatabaseMigrationService( + database = database, + appVersionFromPom = "2.11.0" + ) + + assertThat(databaseMigrationService.migrateToLatestVersion()).isFalse() + + val schemas = repository.find().toList() + assertThat(schemas).hasSize(10) + } } @Test fun `should migrate existing ServerStatusMonitor documents to new collection and cleanup obsolete data`() { - val repository = database.getRepository(Schema::class.java) - repository.insert(Schema(appVersion = "V2.1.0")) + DatabaseConfigurationTestUtils.getTestDatabase().use { database -> + + val repository = database.getRepository(Schema::class.java) + repository.insert(Schema(appVersion = "V2.1.0")) - val databaseMigrationService = DatabaseMigrationService( - database = database, - appVersionFromPom = "2.9.0" - ) + val databaseMigrationService = DatabaseMigrationService( + database = database, + appVersionFromPom = "2.9.0" + ) - val oldCollection = database.getCollection("de.darkatra.vrising.discord.ServerStatusMonitor") - val newCollection = database.getCollection(ObjectUtils.findObjectStoreName(ServerStatusMonitor::class.java)) + val oldCollection = database.getCollection("de.darkatra.vrising.discord.ServerStatusMonitor") + val newCollection = database.getCollection("de.darkatra.vrising.discord.persistence.model.Server") - oldCollection.insert( - arrayOf( + oldCollection.insert( Document.createDocument("hostName", "test-hostname") ) - ) - assertThat(oldCollection.size()).isEqualTo(1) - assertThat(newCollection.size()).isEqualTo(0) + assertThat(oldCollection.size()).isEqualTo(1) + assertThat(newCollection.size()).isEqualTo(0) - assertThat(databaseMigrationService.migrateToLatestVersion()).isTrue() + assertThat(databaseMigrationService.migrateToLatestVersion()).isTrue() - assertThat(oldCollection.size()).isEqualTo(0) - assertThat(newCollection.size()).isEqualTo(1) + assertThat(oldCollection.size()).isEqualTo(0) + assertThat(newCollection.size()).isEqualTo(1) + + val migratedDocument = newCollection.find().first() + assertThat(migratedDocument["hostname"]).isEqualTo("test-hostname") + + val schemas = repository.find().toList() + assertThat(schemas).hasSize(2) + } + } - val migratedDocument = newCollection.find().first() - assertThat(migratedDocument["hostname"]).isEqualTo(migratedDocument["hostName"]) - assertThat(migratedDocument["displayPlayerGearLevel"]).isEqualTo(true) - assertThat(migratedDocument["embedEnabled"]).isEqualTo(true) + @Test + fun `should migrate schema from 1_2_x to 2_11_0`() { + + DatabaseConfigurationTestUtils.getTestDatabase(DatabaseConfigurationTestUtils.DATABASE_FILE_V1_2_x).use { database -> + + val databaseMigrationService = DatabaseMigrationService( + database = database, + appVersionFromPom = "2.11.0" + ) + + val oldCollection = database.getCollection("de.darkatra.vrising.discord.ServerStatusMonitor") + val serverRepository = database.getRepository(Server::class.java) + + assertThat(oldCollection.size()).isEqualTo(1) + assertThat(serverRepository.size()).isEqualTo(0) + + val oldDocument = oldCollection.find().first() + + assertThat(databaseMigrationService.migrateToLatestVersion()).isTrue() + + assertThat(oldCollection.size()).isEqualTo(0) + assertThat(serverRepository.size()).isEqualTo(1) + + val server = serverRepository.find().first() + assertThat(server.id).isEqualTo(oldDocument["id"]) + assertThat(server.version).isNotNull() + assertThat(server.discordServerId).isEqualTo(oldDocument["discordServerId"]) + assertThat(server.hostname).isEqualTo(oldDocument["hostName"]) + assertThat(server.queryPort).isEqualTo(oldDocument["queryPort"]) + assertThat(server.apiHostname).isEqualTo(oldDocument["apiHostname"]) + assertThat(server.apiPort).isEqualTo(oldDocument["apiPort"]) + assertThat(server.apiUsername).isEqualTo(oldDocument["apiUsername"]) + assertThat(server.apiPassword).isEqualTo(oldDocument["apiPassword"]) + assertThat(server.pvpLeaderboard).isNull() + assertThat(server.playerActivityFeed).isNull() + assertThat(server.pvpKillFeed).isNull() + assertThat(server.statusMonitor).isNotNull() + assertThat(server.statusMonitor!!.status).isNotNull() + assertThat(server.statusMonitor!!.status).isEqualTo(Status.ACTIVE) + assertThat(server.statusMonitor!!.discordChannelId).isEqualTo(oldDocument["discordChannelId"]) + assertThat(server.statusMonitor!!.displayServerDescription).isTrue() + assertThat(server.statusMonitor!!.displayPlayerGearLevel).isTrue() + assertThat(server.statusMonitor!!.currentEmbedMessageId).isEqualTo(oldDocument["currentEmbedMessageId"]) + assertThat(server.statusMonitor!!.currentFailedAttempts).isEqualTo(0) + assertThat(server.statusMonitor!!.currentFailedApiAttempts).isEqualTo(0) + assertThat(server.statusMonitor!!.recentErrors).isEmpty() + } + } + + @Test + fun `should migrate schema from 2_10_5 to 2_11_0`() { + + DatabaseConfigurationTestUtils.getTestDatabase(DatabaseConfigurationTestUtils.DATABASE_FILE_V2_10_5).use { database -> + + val repository = database.getRepository(Schema::class.java) + repository.insert(Schema(appVersion = "V2.10.5")) + + val databaseMigrationService = DatabaseMigrationService( + database = database, + appVersionFromPom = "2.11.0" + ) - val schemas = repository.find().toList() - assertThat(schemas).hasSize(2) + val oldCollection = database.getCollection("de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor") + val serverRepository = database.getRepository(Server::class.java) + + assertThat(oldCollection.size()).isEqualTo(1) + assertThat(serverRepository.size()).isEqualTo(0) + + val oldDocument = oldCollection.find().first() + + assertThat(databaseMigrationService.migrateToLatestVersion()).isTrue() + + assertThat(oldCollection.size()).isEqualTo(0) + assertThat(serverRepository.size()).isEqualTo(1) + + val server = serverRepository.find().first() + assertThat(server.id).isEqualTo(oldDocument["id"]) + assertThat(server.version).isEqualTo(oldDocument["version"]) + assertThat(server.discordServerId).isEqualTo(oldDocument["discordServerId"]) + assertThat(server.hostname).isEqualTo(oldDocument["hostname"]) + assertThat(server.queryPort).isEqualTo(oldDocument["queryPort"]) + assertThat(server.apiHostname).isEqualTo(oldDocument["apiHostname"]) + assertThat(server.apiPort).isEqualTo(oldDocument["apiPort"]) + assertThat(server.apiUsername).isEqualTo(oldDocument["apiUsername"]) + assertThat(server.apiPassword).isEqualTo(oldDocument["apiPassword"]) + assertThat(server.pvpLeaderboard).isNull() + assertThat(server.playerActivityFeed).isNotNull() + assertThat(server.playerActivityFeed!!.status).isEqualTo(Status.ACTIVE) + assertThat(server.playerActivityFeed!!.discordChannelId).isEqualTo(oldDocument["playerActivityDiscordChannelId"]) + assertThat(server.playerActivityFeed!!.lastUpdated).isNull() + assertThat(server.playerActivityFeed!!.currentFailedAttempts).isEqualTo(0) + assertThat(server.playerActivityFeed!!.recentErrors).isEmpty() + assertThat(server.pvpKillFeed).isNotNull() + assertThat(server.pvpKillFeed!!.status).isEqualTo(Status.ACTIVE) + assertThat(server.pvpKillFeed!!.discordChannelId).isEqualTo(oldDocument["pvpKillFeedDiscordChannelId"]) + assertThat(server.pvpKillFeed!!.lastUpdated).isNull() + assertThat(server.pvpKillFeed!!.currentFailedAttempts).isEqualTo(0) + assertThat(server.pvpKillFeed!!.recentErrors).isEmpty() + assertThat(server.statusMonitor).isNotNull() + assertThat(server.statusMonitor!!.status).isNotNull() + assertThat(server.statusMonitor!!.status).isEqualTo(Status.ACTIVE) + assertThat(server.statusMonitor!!.discordChannelId).isEqualTo(oldDocument["discordChannelId"]) + assertThat(server.statusMonitor!!.displayServerDescription).isEqualTo(oldDocument["displayServerDescription"]) + assertThat(server.statusMonitor!!.displayPlayerGearLevel).isEqualTo(oldDocument["displayPlayerGearLevel"]) + assertThat(server.statusMonitor!!.currentEmbedMessageId).isEqualTo(oldDocument["currentEmbedMessageId"]) + assertThat(server.statusMonitor!!.currentFailedAttempts).isEqualTo(oldDocument["currentFailedAttempts"]) + assertThat(server.statusMonitor!!.currentFailedApiAttempts).isEqualTo(oldDocument["currentFailedApiAttempts"]) + assertThat(server.statusMonitor!!.recentErrors).isEmpty() + } } } diff --git a/src/test/kotlin/de/darkatra/vrising/discord/persistence/ServerRepositoryTest.kt b/src/test/kotlin/de/darkatra/vrising/discord/persistence/ServerRepositoryTest.kt new file mode 100644 index 0000000..693d290 --- /dev/null +++ b/src/test/kotlin/de/darkatra/vrising/discord/persistence/ServerRepositoryTest.kt @@ -0,0 +1,73 @@ +package de.darkatra.vrising.discord.persistence + +import de.darkatra.vrising.discord.DatabaseConfigurationTestUtils +import de.darkatra.vrising.discord.persistence.model.ServerTestUtils +import org.assertj.core.api.Assertions.assertThat +import org.dizitart.no2.Nitrite +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class ServerRepositoryTest { + + private val nitrite: Nitrite = DatabaseConfigurationTestUtils.getTestDatabase() + + private val serverRepository = ServerRepository(nitrite) + + @BeforeEach + fun setUp() { + DatabaseConfigurationTestUtils.clearDatabase(nitrite) + } + + @Test + fun `should get servers`() { + + serverRepository.addServer( + ServerTestUtils.getServer() + ) + + val serverStatusMonitors = serverRepository.getServers() + + assertThat(serverStatusMonitors).hasSize(1) + + val serverStatusMonitor = serverStatusMonitors.first() + assertThat(serverStatusMonitor.id).isEqualTo(ServerTestUtils.ID) + assertThat(serverStatusMonitor.discordServerId).isEqualTo(ServerTestUtils.DISCORD_SERVER_ID) + assertThat(serverStatusMonitor.hostname).isEqualTo(ServerTestUtils.HOST_NAME) + assertThat(serverStatusMonitor.queryPort).isEqualTo(ServerTestUtils.QUERY_PORT) + } + + @Test + fun `should not update server status monitor with higher version`() { + + val serverStatusMonitor = ServerTestUtils.getServer() + serverRepository.addServer(serverStatusMonitor) + + val update1 = serverRepository.getServer(serverStatusMonitor.id, serverStatusMonitor.discordServerId)!!.apply { + hostname = "test-1" + } + val update2 = serverRepository.getServer(serverStatusMonitor.id, serverStatusMonitor.discordServerId)!!.apply { + hostname = "test-2" + } + + serverRepository.updateServer(update1) + + val e = assertThrows { + serverRepository.updateServer(update2) + } + + assertThat(e.message).isEqualTo("Server with id '${serverStatusMonitor.id}' was already updated by another thread.") + } + + @Test + fun `should not insert server status monitor when using updateServerStatusMonitor`() { + + val e = assertThrows { + serverRepository.updateServer( + ServerTestUtils.getServer() + ) + } + + assertThat(e.message).isEqualTo("Server with id '${ServerTestUtils.ID}' not found.") + } +} diff --git a/src/test/kotlin/de/darkatra/vrising/discord/persistence/model/ServerTestUtils.kt b/src/test/kotlin/de/darkatra/vrising/discord/persistence/model/ServerTestUtils.kt new file mode 100644 index 0000000..f02d199 --- /dev/null +++ b/src/test/kotlin/de/darkatra/vrising/discord/persistence/model/ServerTestUtils.kt @@ -0,0 +1,22 @@ +package de.darkatra.vrising.discord.persistence.model + +import dev.kord.common.entity.Snowflake +import kotlinx.datetime.toKotlinInstant +import java.time.Instant + +object ServerTestUtils { + + const val ID = "id" + val DISCORD_SERVER_ID = Snowflake(Instant.now().toKotlinInstant()).toString() + const val HOST_NAME = "localhost" + const val QUERY_PORT = 8081 + + fun getServer(): Server { + return Server( + id = ID, + discordServerId = DISCORD_SERVER_ID, + hostname = HOST_NAME, + queryPort = QUERY_PORT, + ) + } +} diff --git a/src/test/kotlin/de/darkatra/vrising/discord/serverstatus/ServerStatusMonitorRepositoryTest.kt b/src/test/kotlin/de/darkatra/vrising/discord/serverstatus/ServerStatusMonitorRepositoryTest.kt deleted file mode 100644 index 73f7662..0000000 --- a/src/test/kotlin/de/darkatra/vrising/discord/serverstatus/ServerStatusMonitorRepositoryTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package de.darkatra.vrising.discord.serverstatus - -import de.darkatra.vrising.discord.DatabaseConfigurationTestUtils -import de.darkatra.vrising.discord.persistence.ServerStatusMonitorRepository -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitorStatus -import de.darkatra.vrising.discord.serverstatus.exceptions.OutdatedServerStatusMonitorException -import de.darkatra.vrising.discord.serverstatus.model.ServerStatusMonitorTestUtils -import org.assertj.core.api.Assertions.assertThat -import org.dizitart.no2.Nitrite -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.condition.DisabledInNativeImage - -@DisabledInNativeImage -class ServerStatusMonitorRepositoryTest { - - private val nitrite: Nitrite = DatabaseConfigurationTestUtils.getTestDatabase() - - private val serverStatusMonitorRepository = ServerStatusMonitorRepository(nitrite) - - @BeforeEach - fun setUp() { - DatabaseConfigurationTestUtils.clearDatabase(nitrite) - } - - @Test - fun `should get active server status monitors`() { - - serverStatusMonitorRepository.addServerStatusMonitor( - ServerStatusMonitorTestUtils.getServerStatusMonitor(ServerStatusMonitorStatus.ACTIVE) - ) - - val serverStatusMonitors = serverStatusMonitorRepository.getServerStatusMonitors(status = ServerStatusMonitorStatus.ACTIVE) - - assertThat(serverStatusMonitors).hasSize(1) - - val serverStatusMonitor = serverStatusMonitors.first() - assertThat(serverStatusMonitor.id).isEqualTo(ServerStatusMonitorTestUtils.ID) - assertThat(serverStatusMonitor.discordServerId).isEqualTo(ServerStatusMonitorTestUtils.DISCORD_SERVER_ID) - assertThat(serverStatusMonitor.discordChannelId).isEqualTo(ServerStatusMonitorTestUtils.DISCORD_CHANNEL_ID) - assertThat(serverStatusMonitor.hostname).isEqualTo(ServerStatusMonitorTestUtils.HOST_NAME) - assertThat(serverStatusMonitor.queryPort).isEqualTo(ServerStatusMonitorTestUtils.QUERY_PORT) - } - - @Test - fun `should get no active server status monitors`() { - - serverStatusMonitorRepository.addServerStatusMonitor( - ServerStatusMonitorTestUtils.getServerStatusMonitor(ServerStatusMonitorStatus.INACTIVE) - ) - - val serverStatusMonitors = serverStatusMonitorRepository.getServerStatusMonitors(status = ServerStatusMonitorStatus.ACTIVE) - - assertThat(serverStatusMonitors).hasSize(0) - } - - @Test - fun `should not update server status monitor with higher version`() { - - val serverStatusMonitor = ServerStatusMonitorTestUtils.getServerStatusMonitor(ServerStatusMonitorStatus.ACTIVE) - serverStatusMonitorRepository.addServerStatusMonitor(serverStatusMonitor) - - val update1 = serverStatusMonitorRepository.getServerStatusMonitor(serverStatusMonitor.id, serverStatusMonitor.discordServerId)!!.apply { - status = ServerStatusMonitorStatus.INACTIVE - } - val update2 = serverStatusMonitorRepository.getServerStatusMonitor(serverStatusMonitor.id, serverStatusMonitor.discordServerId)!!.apply { - status = ServerStatusMonitorStatus.ACTIVE - } - - serverStatusMonitorRepository.updateServerStatusMonitor(update1) - - val e = assertThrows { - serverStatusMonitorRepository.updateServerStatusMonitor(update2) - } - - assertThat(e.message).isEqualTo("Monitor with id '${serverStatusMonitor.id}' was already updated by another thread.") - } - - @Test - fun `should not insert server status monitor when using updateServerStatusMonitor`() { - - val e = assertThrows { - serverStatusMonitorRepository.updateServerStatusMonitor( - ServerStatusMonitorTestUtils.getServerStatusMonitor(ServerStatusMonitorStatus.ACTIVE) - ) - } - - assertThat(e.message).isEqualTo("Monitor with id '${ServerStatusMonitorTestUtils.ID}' not found.") - } -} diff --git a/src/test/kotlin/de/darkatra/vrising/discord/serverstatus/model/ServerStatusMonitorTestUtils.kt b/src/test/kotlin/de/darkatra/vrising/discord/serverstatus/model/ServerStatusMonitorTestUtils.kt deleted file mode 100644 index b2e4289..0000000 --- a/src/test/kotlin/de/darkatra/vrising/discord/serverstatus/model/ServerStatusMonitorTestUtils.kt +++ /dev/null @@ -1,29 +0,0 @@ -package de.darkatra.vrising.discord.serverstatus.model - -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitor -import de.darkatra.vrising.discord.persistence.model.ServerStatusMonitorStatus -import dev.kord.common.entity.Snowflake -import kotlinx.datetime.toKotlinInstant -import java.time.Instant - -object ServerStatusMonitorTestUtils { - - const val ID = "id" - val DISCORD_SERVER_ID = Snowflake(Instant.now().toKotlinInstant()).toString() - val DISCORD_CHANNEL_ID = Snowflake(Instant.now().toKotlinInstant()).toString() - const val HOST_NAME = "localhost" - const val QUERY_PORT = 8081 - - fun getServerStatusMonitor(status: ServerStatusMonitorStatus): ServerStatusMonitor { - return ServerStatusMonitor( - id = ID, - discordServerId = DISCORD_SERVER_ID, - discordChannelId = DISCORD_CHANNEL_ID, - hostname = HOST_NAME, - queryPort = QUERY_PORT, - status = status, - displayServerDescription = true, - displayPlayerGearLevel = true - ) - } -} diff --git a/src/test/resources/persistence/v1.2.db b/src/test/resources/persistence/v1.2.db new file mode 100644 index 0000000..1280a64 Binary files /dev/null and b/src/test/resources/persistence/v1.2.db differ diff --git a/src/test/resources/persistence/v2.10.5.db b/src/test/resources/persistence/v2.10.5.db new file mode 100644 index 0000000..9e04b05 Binary files /dev/null and b/src/test/resources/persistence/v2.10.5.db differ