Rewrite config to use Konf (4): Features

The entire config parsing is now rewritten. This entirely removes toml4j
in favor of Konf. It also removes all remaining RawConfig logic.
This commit is contained in:
kageru 2019-10-18 21:56:31 +02:00
parent bb03474bf5
commit d6492bae8f
Signed by: kageru
GPG Key ID: 8282A2BEA4ADA3D2
12 changed files with 71 additions and 119 deletions

View File

@ -36,7 +36,6 @@ val test by tasks.getting(Test::class) {
dependencies { dependencies {
implementation("com.uchuhimo:konf-core:0.20.0") implementation("com.uchuhimo:konf-core:0.20.0")
implementation("com.uchuhimo:konf-toml:0.20.0") implementation("com.uchuhimo:konf-toml:0.20.0")
implementation("com.moandjiezana.toml:toml4j:0.7.2")
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
implementation("org.javacord:javacord:3.0.4") implementation("org.javacord:javacord:3.0.4")
implementation("org.mapdb:mapdb:3.0.7") implementation("org.mapdb:mapdb:3.0.7")

View File

@ -3,7 +3,6 @@ package moe.kageru.kagebot
import moe.kageru.kagebot.Util.checked import moe.kageru.kagebot.Util.checked
import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.config.Config
import moe.kageru.kagebot.config.ConfigParser import moe.kageru.kagebot.config.ConfigParser
import moe.kageru.kagebot.config.RawConfig
import moe.kageru.kagebot.cron.CronD import moe.kageru.kagebot.cron.CronD
import moe.kageru.kagebot.persistence.Dao import moe.kageru.kagebot.persistence.Dao
import org.javacord.api.DiscordApiBuilder import org.javacord.api.DiscordApiBuilder
@ -44,7 +43,7 @@ object Kagebot {
val api = DiscordApiBuilder().setToken(secret).login().join() val api = DiscordApiBuilder().setToken(secret).login().join()
Globals.api = api Globals.api = api
try { try {
ConfigParser.initialLoad(RawConfig.DEFAULT_CONFIG_PATH) ConfigParser.initialLoad(ConfigParser.DEFAULT_CONFIG_PATH)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
println("Config error:\n$e,\n${e.message},\n${e.stackTrace.joinToString("\n")}") println("Config error:\n$e,\n${e.message},\n${e.stackTrace.joinToString("\n")}")
exitProcess(1) exitProcess(1)

View File

@ -10,11 +10,15 @@ object Config {
val systemSpec = Config { addSpec(SystemSpec) }.from.toml val systemSpec = Config { addSpec(SystemSpec) }.from.toml
val localeSpec = Config { addSpec(LocalizationSpec) }.from.toml val localeSpec = Config { addSpec(LocalizationSpec) }.from.toml
val commandSpec = Config { addSpec(CommandSpec) }.from.toml val commandSpec = Config { addSpec(CommandSpec) }.from.toml
val featureSpec = Config { addSpec(FeatureSpec) }.from.toml
lateinit var system: Config lateinit var system: Config
lateinit var localization: Config lateinit var localization: Config
lateinit var server: Server
lateinit var commandConfig: Config lateinit var commandConfig: Config
lateinit var features: Features lateinit var featureConfig: Config
lateinit var server: Server
// for easier access // for easier access
val features: Features get() = featureConfig[FeatureSpec.features]
val commands: List<Command> get() = commandConfig[CommandSpec.command] val commands: List<Command> get() = commandConfig[CommandSpec.command]
} }

View File

@ -2,26 +2,29 @@ package moe.kageru.kagebot.config
import moe.kageru.kagebot.Globals import moe.kageru.kagebot.Globals
import moe.kageru.kagebot.config.SystemSpec.serverId import moe.kageru.kagebot.config.SystemSpec.serverId
import moe.kageru.kagebot.features.Features
import java.io.File import java.io.File
object ConfigParser { object ConfigParser {
val configFile: File = File(RawConfig.DEFAULT_CONFIG_PATH) internal const val DEFAULT_CONFIG_PATH = "config.toml"
val configFile: File = File(DEFAULT_CONFIG_PATH)
fun initialLoad(file: String) { fun initialLoad(file: String) {
val rawConfig = RawConfig.read(file) val configFile = getFile(file)
val configFile = RawConfig.getFile(file)
val config = Config.systemSpec.file(configFile) val config = Config.systemSpec.file(configFile)
Config.system = config Config.system = config
Config.server = Globals.api.getServerById(config[serverId]) Config.server = Globals.api.getServerById(config[serverId])
.orElseThrow { IllegalArgumentException("Invalid server configured.") } .orElseThrow { IllegalArgumentException("Invalid server configured.") }
Config.localization = Config.localeSpec.file(configFile) Config.localization = Config.localeSpec.file(configFile)
reloadFeatures(rawConfig) Config.featureConfig = Config.featureSpec.file(configFile)
Config.commandConfig = Config.commandSpec.file(configFile) Config.commandConfig = Config.commandSpec.file(configFile)
} }
fun reloadFeatures(rawConfig: RawConfig) { private fun getFile(path: String): File {
Config.features = rawConfig.features?.let(::Features) val file = File(path)
?: Features(RawFeatures(null, null)) if (file.isFile) {
return file
}
println("Config not found, falling back to defaults...")
return File(this::class.java.classLoader.getResource(path)!!.toURI())
} }
} }

View File

@ -0,0 +1,28 @@
package moe.kageru.kagebot.config
import com.uchuhimo.konf.ConfigSpec
import moe.kageru.kagebot.command.Command
import moe.kageru.kagebot.config.Config.system
import moe.kageru.kagebot.features.Features
import java.awt.Color
object SystemSpec : ConfigSpec() {
private val rawColor by optional("#1793d0", name = "color")
val serverId by required<String>()
val color by kotlin.lazy { Color.decode(system[rawColor])!! }
}
object LocalizationSpec : ConfigSpec() {
val permissionDenied by optional("You do not have the permission to use this command.")
val redirectedMessage by optional("says")
val messageDeleted by optional("Your message was deleted.")
val timeout by optional("You have been timed out for @@ minutes.")
}
object CommandSpec : ConfigSpec(prefix = "") {
val command by optional(emptyList<Command>())
}
object FeatureSpec : ConfigSpec(prefix = "") {
val features by optional(Features.DEFAULT, name = "feature")
}

View File

@ -1,48 +0,0 @@
package moe.kageru.kagebot.config
import com.google.gson.annotations.SerializedName
import com.moandjiezana.toml.Toml
import com.uchuhimo.konf.ConfigSpec
import moe.kageru.kagebot.command.Command
import moe.kageru.kagebot.config.Config.system
import java.awt.Color
import java.io.File
class RawConfig(@SerializedName("feature") val features: RawFeatures?) {
companion object {
const val DEFAULT_CONFIG_PATH = "config.toml"
fun readFromString(tomlContent: String): RawConfig = Toml().read(tomlContent).to(RawConfig::class.java)
fun getFile(path: String): File {
val file = File(path)
if (file.isFile) {
return file
}
println("Config not found, falling back to defaults...")
return File(this::class.java.classLoader.getResource(path)!!.toURI())
}
fun read(path: String = DEFAULT_CONFIG_PATH): RawConfig {
val toml: Toml = Toml().read(getFile(path))
return toml.to(RawConfig::class.java)
}
}
}
object SystemSpec : ConfigSpec() {
private val rawColor by optional("#1793d0", name = "color")
val serverId by required<String>()
val color by kotlin.lazy { Color.decode(system[rawColor])!! }
}
object LocalizationSpec : ConfigSpec() {
val permissionDenied by optional("You do not have the permission to use this command.")
val redirectedMessage by optional("says")
val messageDeleted by optional("Your message was deleted.")
val timeout by optional("You have been timed out for @@ minutes.")
}
object CommandSpec : ConfigSpec(prefix = "") {
val command by optional(emptyList<Command>())
}

View File

@ -1,5 +0,0 @@
package moe.kageru.kagebot.config
class RawFeatures(val welcome: RawWelcomeFeature?, val timeout: RawTimeoutFeature?)
class RawWelcomeFeature(val content: List<String>?, val fallbackChannel: String?, val fallbackMessage: String?)
class RawTimeoutFeature(val role: String?)

View File

@ -1,23 +1,10 @@
package moe.kageru.kagebot.features package moe.kageru.kagebot.features
import moe.kageru.kagebot.config.RawFeatures class Features(val welcome: WelcomeFeature?, val timeout: TimeoutFeature?) {
private val debug = DebugFeature()
class Features( private val help = HelpFeature()
val welcome: WelcomeFeature?, private val getConfig = GetConfigFeature()
debug: DebugFeature, private val setConfig = SetConfigFeature()
help: HelpFeature,
getConfig: GetConfigFeature,
setConfig: SetConfigFeature,
val timeout: TimeoutFeature?
) {
constructor(rawFeatures: RawFeatures) : this(
rawFeatures.welcome?.let(::WelcomeFeature),
DebugFeature(),
HelpFeature(),
GetConfigFeature(),
SetConfigFeature(),
rawFeatures.timeout?.let(::TimeoutFeature)
)
private val all = listOf(welcome, debug, help, getConfig, setConfig, timeout) private val all = listOf(welcome, debug, help, getConfig, setConfig, timeout)
private val featureMap = mapOf( private val featureMap = mapOf(
@ -31,4 +18,8 @@ class Features(
fun findByString(feature: String) = featureMap[feature] fun findByString(feature: String) = featureMap[feature]
fun eventFeatures() = all.filterIsInstance<EventFeature>() fun eventFeatures() = all.filterIsInstance<EventFeature>()
companion object {
val DEFAULT = Features(null, null)
}
} }

View File

@ -1,12 +1,10 @@
package moe.kageru.kagebot.features package moe.kageru.kagebot.features
import moe.kageru.kagebot.Log
import moe.kageru.kagebot.MessageUtil.sendEmbed import moe.kageru.kagebot.MessageUtil.sendEmbed
import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.config.Config
import moe.kageru.kagebot.config.ConfigParser import moe.kageru.kagebot.config.ConfigParser
import moe.kageru.kagebot.config.RawConfig
import org.javacord.api.entity.channel.TextChannel
import org.javacord.api.event.message.MessageCreateEvent import org.javacord.api.event.message.MessageCreateEvent
import java.lang.reflect.InvocationTargetException
class SetConfigFeature : MessageFeature { class SetConfigFeature : MessageFeature {
@ExperimentalStdlibApi @ExperimentalStdlibApi
@ -16,33 +14,16 @@ class SetConfigFeature : MessageFeature {
return return
} }
val newConfig = message.messageAttachments[0].url.openStream().readAllBytes().decodeToString() val newConfig = message.messageAttachments[0].url.openStream().readAllBytes().decodeToString()
val rawConfig = try {
RawConfig.readFromString(newConfig)
} catch (e: IllegalStateException) {
reportError(message.channel, e)
return
}
try { try {
Config.localization = Config.localeSpec.string(newConfig) Config.localization = Config.localeSpec.string(newConfig)
ConfigParser.reloadFeatures(rawConfig) Config.featureConfig = Config.featureSpec.string(newConfig)
Config.commandConfig = Config.commandSpec.string(newConfig) Config.commandConfig = Config.commandSpec.string(newConfig)
ConfigParser.configFile.writeText(newConfig) ConfigParser.configFile.writeText(newConfig)
message.channel.sendMessage("Config reloaded.") message.channel.sendMessage("Config reloaded.")
} catch (e: IllegalArgumentException) { } catch (e: Exception) {
message.channel.sendEmbed { message.channel.sendEmbed {
addField("Error", "```${e.message}```") addField("Error", "```${e.message}```")
} }
} }
} }
private fun reportError(message: TextChannel, e: IllegalStateException) {
message.sendEmbed {
addField(
"An unexpected error occured. This is probably caused by a malformed config file. Perhaps this can help:",
"```$e: ${e.message}"
)
}
Log.info("Could not parse new config: $e: ${e.message}")
return
}
} }

View File

@ -1,5 +1,6 @@
package moe.kageru.kagebot.features package moe.kageru.kagebot.features
import com.fasterxml.jackson.annotation.JsonProperty
import moe.kageru.kagebot.Log import moe.kageru.kagebot.Log
import moe.kageru.kagebot.MessageUtil.sendEmbed import moe.kageru.kagebot.MessageUtil.sendEmbed
import moe.kageru.kagebot.Util.findRole import moe.kageru.kagebot.Util.findRole
@ -7,16 +8,14 @@ import moe.kageru.kagebot.Util.findUser
import moe.kageru.kagebot.Util.ifNotEmpty import moe.kageru.kagebot.Util.ifNotEmpty
import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.config.Config
import moe.kageru.kagebot.config.LocalizationSpec import moe.kageru.kagebot.config.LocalizationSpec
import moe.kageru.kagebot.config.RawTimeoutFeature
import moe.kageru.kagebot.persistence.Dao import moe.kageru.kagebot.persistence.Dao
import org.javacord.api.entity.permission.Role import org.javacord.api.entity.permission.Role
import org.javacord.api.event.message.MessageCreateEvent import org.javacord.api.event.message.MessageCreateEvent
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
class TimeoutFeature(raw: RawTimeoutFeature) : MessageFeature { class TimeoutFeature(@JsonProperty("role") role: String) : MessageFeature {
private val timeoutRole: Role = raw.role?.let(::findRole) private val timeoutRole: Role = findRole(role)
?: throw IllegalArgumentException("No timeout role defined")
override fun handle(message: MessageCreateEvent) { override fun handle(message: MessageCreateEvent) {
val timeout = message.readableMessageContent.split(' ', limit = 4).let { args -> val timeout = message.readableMessageContent.split(' ', limit = 4).let { args ->

View File

@ -5,14 +5,19 @@ import moe.kageru.kagebot.MessageUtil
import moe.kageru.kagebot.Util import moe.kageru.kagebot.Util
import moe.kageru.kagebot.Util.checked import moe.kageru.kagebot.Util.checked
import moe.kageru.kagebot.Util.failed import moe.kageru.kagebot.Util.failed
import moe.kageru.kagebot.config.RawWelcomeFeature
import org.javacord.api.DiscordApi import org.javacord.api.DiscordApi
import org.javacord.api.entity.channel.TextChannel import org.javacord.api.entity.channel.TextChannel
import org.javacord.api.entity.message.embed.EmbedBuilder import org.javacord.api.entity.message.embed.EmbedBuilder
import org.javacord.api.event.message.MessageCreateEvent import org.javacord.api.event.message.MessageCreateEvent
import org.javacord.api.event.server.member.ServerMemberJoinEvent import org.javacord.api.event.server.member.ServerMemberJoinEvent
class WelcomeFeature(rawWelcome: RawWelcomeFeature) : MessageFeature, EventFeature { class WelcomeFeature(
content: List<String>?,
fallbackChannel: String?,
private val fallbackMessage: String?
) : MessageFeature, EventFeature {
val embed: EmbedBuilder? by lazy { content?.let(MessageUtil::listToEmbed) }
override fun register(api: DiscordApi) { override fun register(api: DiscordApi) {
api.addServerMemberJoinListener { event -> api.addServerMemberJoinListener { event ->
checked { welcomeUser(event) } checked { welcomeUser(event) }
@ -40,14 +45,10 @@ class WelcomeFeature(rawWelcome: RawWelcomeFeature) : MessageFeature, EventFeatu
private fun hasFallback(): Boolean = fallbackChannel != null && fallbackMessage != null private fun hasFallback(): Boolean = fallbackChannel != null && fallbackMessage != null
val embed: EmbedBuilder? by lazy { private val fallbackChannel: TextChannel? = fallbackChannel?.let {
rawWelcome.content?.let(MessageUtil::listToEmbed) requireNotNull(fallbackMessage) {
}
private val fallbackChannel: TextChannel? = rawWelcome.fallbackChannel?.let {
requireNotNull(rawWelcome.fallbackMessage) {
"[feature.welcome.fallbackMessage] must not be null if fallbackChannel is defined" "[feature.welcome.fallbackMessage] must not be null if fallbackChannel is defined"
} }
Util.findChannel(it) Util.findChannel(it)
} }
private val fallbackMessage: String? = rawWelcome.fallbackMessage
} }

View File

@ -17,7 +17,7 @@ class ConfigTest : ShouldSpec({
"should properly parse test config" { "should properly parse test config" {
Config.system[SystemSpec.serverId] shouldNotBe null Config.system[SystemSpec.serverId] shouldNotBe null
SystemSpec.color shouldBe Color.decode("#1793d0") SystemSpec.color shouldBe Color.decode("#1793d0")
Config.features shouldNotBe null Config.features.welcome!!.embed shouldNotBe null
Config.commands.size shouldBe 3 Config.commands.size shouldBe 3
} }