From 7ebafc995829f7a44cfbd113fc6a63712cd6f917 Mon Sep 17 00:00:00 2001 From: kageru Date: Wed, 12 Jun 2019 23:43:36 +0200 Subject: [PATCH] Completely rewrote config Added a rawconfig as an intermediate step when parsing the TOML. This will make nullability much more reliable and also gives me lots of options in the future. It also broke all tests because reading the config (not the raw config) now depends on the discord API. :tehe: --- src/main/kotlin/moe/kageru/kagebot/Command.kt | 79 ------------------- src/main/kotlin/moe/kageru/kagebot/Config.kt | 43 ---------- src/main/kotlin/moe/kageru/kagebot/Globals.kt | 11 +++ src/main/kotlin/moe/kageru/kagebot/Kagebot.kt | 48 +++++++++-- .../kotlin/moe/kageru/kagebot/MessageUtil.kt | 20 ++++- src/main/kotlin/moe/kageru/kagebot/Util.kt | 53 ++++++++++++- .../moe/kageru/kagebot/command/Command.kt | 62 +++++++++++++++ .../kagebot/{ => command}/MessageActions.kt | 40 +++++++--- .../moe/kageru/kagebot/command/Permissions.kt | 34 ++++++++ .../moe/kageru/kagebot/config/Config.kt | 47 +++++++++++ .../moe/kageru/kagebot/config/RawConfig.kt | 50 ++++++++++++ .../moe/kageru/kagebot/features/Features.kt | 27 +++++++ src/main/kotlin/moe/kageru/kagebot/main.kt | 6 +- src/main/resources/config.toml | 25 ++++-- .../kotlin/moe/kageru/kagebot/CommandTest.kt | 2 +- .../kotlin/moe/kageru/kagebot/ConfigTest.kt | 6 ++ .../kotlin/moe/kageru/kagebot/FeatureTest.kt | 45 +++++++++++ .../kotlin/moe/kageru/kagebot/TestUtil.kt | 2 +- 18 files changed, 442 insertions(+), 158 deletions(-) delete mode 100644 src/main/kotlin/moe/kageru/kagebot/Command.kt delete mode 100644 src/main/kotlin/moe/kageru/kagebot/Config.kt create mode 100644 src/main/kotlin/moe/kageru/kagebot/Globals.kt create mode 100644 src/main/kotlin/moe/kageru/kagebot/command/Command.kt rename src/main/kotlin/moe/kageru/kagebot/{ => command}/MessageActions.kt (51%) create mode 100644 src/main/kotlin/moe/kageru/kagebot/command/Permissions.kt create mode 100644 src/main/kotlin/moe/kageru/kagebot/config/Config.kt create mode 100644 src/main/kotlin/moe/kageru/kagebot/config/RawConfig.kt create mode 100644 src/main/kotlin/moe/kageru/kagebot/features/Features.kt create mode 100644 src/test/kotlin/moe/kageru/kagebot/FeatureTest.kt diff --git a/src/main/kotlin/moe/kageru/kagebot/Command.kt b/src/main/kotlin/moe/kageru/kagebot/Command.kt deleted file mode 100644 index 4b39210..0000000 --- a/src/main/kotlin/moe/kageru/kagebot/Command.kt +++ /dev/null @@ -1,79 +0,0 @@ -package moe.kageru.kagebot - -import com.google.gson.annotations.SerializedName -import moe.kageru.kagebot.Config.Companion.config -import moe.kageru.kagebot.Util.doIf -import org.javacord.api.entity.message.MessageAuthor -import org.javacord.api.event.message.MessageCreateEvent - -private const val AUTHOR_PLACEHOLDER = "@@" - -class Command( - trigger: String?, - private val response: String?, - matchType: MatchType?, - private val permissions: Permissions?, - @SerializedName("action") private val actions: MessageActions? -) { - val trigger: String = trigger!! - val regex: Regex? = if (matchType == MatchType.REGEX) Regex(trigger!!) else null - val matchType: MatchType = matchType ?: MatchType.PREFIX - - constructor(cmd: Command) : this( - cmd.trigger, - cmd.response, - cmd.matchType, - cmd.permissions?.let { Permissions(it) }, - cmd.actions - ) - - fun execute(message: MessageCreateEvent) { - if (!(message.messageAuthor.isBotOwner || permissions?.isAllowed(message) != false)) { - message.channel.sendMessage(config.localization.permissionDenied) - return - } - this.actions?.run(message, this) - this.response?.let { - message.channel.sendMessage(respond(message.messageAuthor)) - } - } - - fun matches(msg: String) = this.matchType.matches(msg, this) - private fun respond(author: MessageAuthor) = this.response!!.doIf({ it.contains(AUTHOR_PLACEHOLDER) }) { - it.replace(AUTHOR_PLACEHOLDER, MessageUtil.mention(author)) - } -} - -enum class MatchType { - PREFIX { - override fun matches(message: String, command: Command) = message.startsWith(command.trigger) - }, - CONTAINS { - override fun matches(message: String, command: Command) = message.contains(command.trigger) - }, - REGEX { - override fun matches(message: String, command: Command) = command.regex!!.matches(message) - }; - - abstract fun matches(message: String, command: Command): Boolean -} - -class Permissions(hasOneOf: Iterable?, hasNoneOf: Iterable?, private val onlyDM: Boolean) { - private val hasNoneOf = hasNoneOf?.toSet() - private val hasOneOf = hasOneOf?.toSet() - - constructor(perms: Permissions) : this(perms.hasOneOf, perms.hasNoneOf, perms.onlyDM) - - fun isAllowed(message: MessageCreateEvent): Boolean { - if (onlyDM && !message.isPrivateMessage) { - return false - } - hasOneOf?.let { - if (!Util.hasOneOf(message.messageAuthor, hasOneOf)) return false - } - hasNoneOf?.let { - if (Util.hasOneOf(message.messageAuthor, hasNoneOf)) return false - } - return true - } -} \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kagebot/Config.kt b/src/main/kotlin/moe/kageru/kagebot/Config.kt deleted file mode 100644 index 10befd5..0000000 --- a/src/main/kotlin/moe/kageru/kagebot/Config.kt +++ /dev/null @@ -1,43 +0,0 @@ -package moe.kageru.kagebot - -import com.google.gson.annotations.SerializedName -import com.moandjiezana.toml.Toml -import org.javacord.api.DiscordApi -import org.javacord.api.entity.server.Server -import java.io.File - -class Config( - val system: System, - val localization: Localization, - @SerializedName("command") val commands: List -) { - companion object { - val config: Config by lazy { read("config.toml") } - val secret = File("secret").readText().replace("\n", "") - var server: Server? = null - get() = field!! - var api: DiscordApi? = null - get() = field!! - - private fun read(path: String): Config { - val rawConfig: Toml = Toml().read(run { - val file = File(path) - if (file.isFile) { - return@run file - } - println("Config not found, falling back to defaults...") - File(this::class.java.classLoader.getResource(path)!!.toURI()) - }) - val parsed = rawConfig.to(Config::class.java) - return Config( - parsed.system, - parsed.localization, - parsed.commands.map { Command(it) } - ) - } - - } -} - -data class System(val serverId: String, val color: String) -data class Localization(val permissionDenied: String, val redirectedMessage: String, val messageDeleted: String) \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kagebot/Globals.kt b/src/main/kotlin/moe/kageru/kagebot/Globals.kt new file mode 100644 index 0000000..b2be09b --- /dev/null +++ b/src/main/kotlin/moe/kageru/kagebot/Globals.kt @@ -0,0 +1,11 @@ +package moe.kageru.kagebot + +import moe.kageru.kagebot.config.Config +import org.javacord.api.DiscordApi +import org.javacord.api.entity.server.Server + +object Globals { + lateinit var server: Server + lateinit var api: DiscordApi + lateinit var config: Config +} \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kagebot/Kagebot.kt b/src/main/kotlin/moe/kageru/kagebot/Kagebot.kt index 2ccdf3e..4560b0b 100644 --- a/src/main/kotlin/moe/kageru/kagebot/Kagebot.kt +++ b/src/main/kotlin/moe/kageru/kagebot/Kagebot.kt @@ -1,9 +1,12 @@ package moe.kageru.kagebot -import moe.kageru.kagebot.Config.Companion.config import moe.kageru.kagebot.Log.log +import moe.kageru.kagebot.config.Config +import moe.kageru.kagebot.config.RawConfig import org.javacord.api.DiscordApiBuilder import org.javacord.api.event.message.MessageCreateEvent +import org.javacord.api.event.server.member.ServerMemberJoinEvent +import java.io.File class Kagebot { companion object { @@ -11,24 +14,53 @@ class Kagebot { if (event.messageAuthor.isYourself) { return } - for (command in config.commands) { + for (command in Globals.config.commands) { if (command.matches(event.messageContent)) { command.execute(event) break } } } + + fun welcomeUser(event: ServerMemberJoinEvent) { + Globals.config.features.welcome!!.let { welcome -> + val message = event.user.sendMessage(welcome.embed) + // If the user disabled direct messages, try the fallback (if defined) + if (message.isCompletedExceptionally + && welcome.fallbackChannel != null + && welcome.fallbackMessage != null + ) { + welcome.fallbackChannel.sendMessage( + welcome.fallbackMessage.replace( + "@@", + MessageUtil.mention(event.user) + ) + ) + } + } + } } init { - val api = DiscordApiBuilder().setToken(Config.secret).login().join() - Config.server = api.getServerById(config.system.serverId).orElseThrow() - Config.api = api + Globals.api = DiscordApiBuilder().setToken(getSecret()).login().join() + try { + Globals.config = Config(RawConfig.read()) + } catch (e: IllegalArgumentException) { + println("Config error:\n$e,\n${e.message},\n${e.stackTrace}") + System.exit(1) + } Runtime.getRuntime().addShutdownHook(Thread { log.info("Bot has been interrupted. Shutting down.") - api.disconnect() + Globals.api.disconnect() }) log.info("kagebot Mk II running") - api.addMessageCreateListener { processMessage(it) } + Globals.api.addMessageCreateListener { processMessage(it) } + Globals.config.features.welcome?.let { welcome -> + if (welcome.enabled) { + Globals.api.addServerMemberJoinListener { welcomeUser(it) } + } + } } -} \ No newline at end of file + + private fun getSecret() = File("secret").readText().replace("\n", "") +} diff --git a/src/main/kotlin/moe/kageru/kagebot/MessageUtil.kt b/src/main/kotlin/moe/kageru/kagebot/MessageUtil.kt index 9f95df8..dd7bc5c 100644 --- a/src/main/kotlin/moe/kageru/kagebot/MessageUtil.kt +++ b/src/main/kotlin/moe/kageru/kagebot/MessageUtil.kt @@ -1,18 +1,30 @@ package moe.kageru.kagebot -import moe.kageru.kagebot.Config.Companion.config import org.javacord.api.entity.message.MessageAuthor import org.javacord.api.entity.message.embed.EmbedBuilder -import java.awt.Color +import org.javacord.api.entity.user.User object MessageUtil { fun mention(user: MessageAuthor): String { return "<@${user.id}>" } + fun mention(user: User): String { + return "<@${user.id}>" + } + + fun getEmbedBuilder(): EmbedBuilder { val builder = EmbedBuilder() - Config.server!!.icon.ifPresent { builder.setThumbnail(it) } - return builder.setColor(Color.decode(config.system.color)).setTimestampToNow() + Globals.server.icon.ifPresent { builder.setThumbnail(it) } + return builder.setColor(Globals.config.system.color).setTimestampToNow() + } + + fun mapToEmbed(contents: Map): EmbedBuilder { + val builder = getEmbedBuilder() + for ((heading, content) in contents) { + builder.addField(heading, content) + } + return builder } } \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kagebot/Util.kt b/src/main/kotlin/moe/kageru/kagebot/Util.kt index 8a25ef1..e6d268c 100644 --- a/src/main/kotlin/moe/kageru/kagebot/Util.kt +++ b/src/main/kotlin/moe/kageru/kagebot/Util.kt @@ -1,6 +1,10 @@ package moe.kageru.kagebot +import moe.kageru.kagebot.Globals.api +import moe.kageru.kagebot.Globals.server +import org.javacord.api.entity.channel.TextChannel import org.javacord.api.entity.message.MessageAuthor +import org.javacord.api.entity.permission.Role import java.util.* object Util { @@ -8,6 +12,10 @@ object Util { return if (condition(this)) op(this) else this } + /** + * Mimics the behavior of [Optional.ifPresent], but returns null if the optional is empty, + * allowing easier fallback behavior via Kotlin’s ?: operator. + */ inline fun Optional.ifNotEmpty(op: (T) -> R): R? { if (this.isPresent) { return op(this.get()) @@ -15,9 +23,50 @@ object Util { return null } - fun hasOneOf(messageAuthor: MessageAuthor, roles: Set): Boolean { + fun hasOneOf(messageAuthor: MessageAuthor, roles: Set): Boolean { return messageAuthor.asUser().ifNotEmpty { user -> - user.getRoles(Config.server).map { it.id }.toSet().intersect(roles).isNotEmpty() + user.getRoles(server).map { it }.toSet().intersect(roles).isNotEmpty() } ?: false } + + private val channelIdRegex = Regex("\\d{18}") + fun String.isEntityId() = channelIdRegex.matches(this) + + @Throws(IllegalArgumentException::class) + fun findRole(idOrName: String): Role { + return when { + idOrName.isEntityId() -> server.getRoleById(idOrName).ifNotEmpty { it } + ?: throw IllegalArgumentException("Role $idOrName not found.") + else -> server.getRolesByNameIgnoreCase(idOrName).let { + when (it.size) { + 0 -> throw IllegalArgumentException("Role $idOrName not found.") + 1 -> it[0] + else -> throw IllegalArgumentException("More than one role found with name $idOrName. Please specify the role ID instead") + } + } + } + } + + @Throws(IllegalArgumentException::class) + fun findChannel(idOrName: String): TextChannel { + return when { + idOrName.isEntityId() -> server.getTextChannelById(idOrName).ifNotEmpty { it } + ?: throw IllegalArgumentException("Channel ID $idOrName not found.") + else -> if (idOrName.startsWith('@')) { + api.getCachedUserByDiscriminatedName(idOrName.removePrefix("@")).ifNotEmpty { user -> + user.privateChannel.ifNotEmpty { it } + ?: throw IllegalArgumentException("Could not open private channel with user $idOrName for redirection.") + } + ?: throw IllegalArgumentException("Can’t find user $idOrName for redirection.") + } else { + server.getTextChannelsByName(idOrName).let { + when (it.size) { + 0 -> throw IllegalArgumentException("Channel $idOrName not found.") + 1 -> it[0] + else -> throw IllegalArgumentException("More than one channel found with name $idOrName. Please specify the channel ID instead") + } + } + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kagebot/command/Command.kt b/src/main/kotlin/moe/kageru/kagebot/command/Command.kt new file mode 100644 index 0000000..e28c8b2 --- /dev/null +++ b/src/main/kotlin/moe/kageru/kagebot/command/Command.kt @@ -0,0 +1,62 @@ +package moe.kageru.kagebot.command + +import moe.kageru.kagebot.Globals.config +import moe.kageru.kagebot.MessageUtil +import moe.kageru.kagebot.Util.doIf +import moe.kageru.kagebot.config.RawCommand +import org.javacord.api.entity.message.MessageAuthor +import org.javacord.api.event.message.MessageCreateEvent + +private const val AUTHOR_PLACEHOLDER = "@@" + +class Command(cmd: RawCommand) { + val trigger: String + private val response: String? + val matchType: MatchType + private val permissions: Permissions? + private val actions: MessageActions? + val regex: Regex? + + init { + trigger = cmd.trigger!! + response = cmd.response + matchType = cmd.matchType?.let { type -> + MatchType.values().find { it.name.equals(type, ignoreCase = true) } + ?: throw IllegalArgumentException("Invalid [command.matchType]: “${cmd.matchType}”") + } ?: MatchType.PREFIX + permissions = cmd.permissions?.let { Permissions(it) } + actions = cmd.actions?.let { MessageActions(it) } + regex = if (matchType == MatchType.REGEX) Regex(trigger) else null + } + + fun execute(message: MessageCreateEvent) { + if (permissions?.isAllowed(message) == false) { + message.channel.sendMessage(config.localization.permissionDenied) + return + } + this.actions?.run(message, this) + this.response?.let { + message.channel.sendMessage(respond(message.messageAuthor)) + } + } + + fun matches(msg: String) = this.matchType.matches(msg, this) + private fun respond(author: MessageAuthor) = this.response!!.doIf({ it.contains(AUTHOR_PLACEHOLDER) }) { + it.replace(AUTHOR_PLACEHOLDER, MessageUtil.mention(author)) + } +} + +enum class MatchType { + PREFIX { + override fun matches(message: String, command: Command) = message.startsWith(command.trigger) + }, + CONTAINS { + override fun matches(message: String, command: Command) = message.contains(command.trigger) + }, + REGEX { + override fun matches(message: String, command: Command) = command.regex!!.matches(message) + }; + + abstract fun matches(message: String, command: Command): Boolean +} + diff --git a/src/main/kotlin/moe/kageru/kagebot/MessageActions.kt b/src/main/kotlin/moe/kageru/kagebot/command/MessageActions.kt similarity index 51% rename from src/main/kotlin/moe/kageru/kagebot/MessageActions.kt rename to src/main/kotlin/moe/kageru/kagebot/command/MessageActions.kt index a0d8eba..ced5954 100644 --- a/src/main/kotlin/moe/kageru/kagebot/MessageActions.kt +++ b/src/main/kotlin/moe/kageru/kagebot/command/MessageActions.kt @@ -1,27 +1,45 @@ -package moe.kageru.kagebot +package moe.kageru.kagebot.command -import moe.kageru.kagebot.Config.Companion.config +import moe.kageru.kagebot.Globals.config import moe.kageru.kagebot.Log.log -import moe.kageru.kagebot.Util.ifNotEmpty +import moe.kageru.kagebot.MessageUtil +import moe.kageru.kagebot.Util +import moe.kageru.kagebot.config.RawMessageActions +import moe.kageru.kagebot.config.RawRedirect +import org.javacord.api.entity.channel.TextChannel import org.javacord.api.event.message.MessageCreateEvent -class MessageActions(private val delete: Boolean, private val redirect: Redirect?) { +class MessageActions(rawActions: RawMessageActions) { + private val delete: Boolean = rawActions.delete + private val redirect: Redirect? = rawActions.redirect?.let { Redirect(it) } + fun run(message: MessageCreateEvent, command: Command) { - if (delete && message.message.canYouDelete()) { + if (delete) { + deleteMessage(message) + } + redirect?.execute(message, command) + } + + private fun deleteMessage(message: MessageCreateEvent) { + if (message.message.canYouDelete()) { message.deleteMessage() - message.messageAuthor.asUser().ifNotEmpty { user -> + message.messageAuthor.asUser().ifPresent { user -> user.sendMessage( MessageUtil.getEmbedBuilder() .addField("Blacklisted", config.localization.messageDeleted) .addField("Original:", "“${message.readableMessageContent}”") ) } + } else { + log.info("Tried to delete a message without the necessary permissions. Channel: ${message.channel.id}") } - redirect?.execute(message, command) } } -class Redirect(private val target: Long, private val anonymous: Boolean) { +class Redirect(rawRedirect: RawRedirect) { + private val target: TextChannel = rawRedirect.target?.let(Util::findChannel) + ?: throw IllegalArgumentException("Every redirect needs to have a target.") + private val anonymous: Boolean = rawRedirect.anonymous fun execute(message: MessageCreateEvent, command: Command) { val embed = MessageUtil.getEmbedBuilder() @@ -43,7 +61,9 @@ class Redirect(private val target: Long, private val anonymous: Boolean) { setAuthor(message.messageAuthor) } } - Config.server!!.getTextChannelById(target).ifNotEmpty { it.sendMessage(embed) } - ?: log.warning("Could not redirect message to channel $target") + + if (target.sendMessage(embed).isCompletedExceptionally) { + log.warning("Could not redirect message to channel $target") + } } } \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kagebot/command/Permissions.kt b/src/main/kotlin/moe/kageru/kagebot/command/Permissions.kt new file mode 100644 index 0000000..7052b9c --- /dev/null +++ b/src/main/kotlin/moe/kageru/kagebot/command/Permissions.kt @@ -0,0 +1,34 @@ +package moe.kageru.kagebot.command + +import moe.kageru.kagebot.Util +import moe.kageru.kagebot.config.RawPermissions +import org.javacord.api.entity.permission.Role +import org.javacord.api.event.message.MessageCreateEvent + +class Permissions(perms: RawPermissions) { + private val hasOneOf: Set? + private val hasNoneOf: Set? + private val onlyDM: Boolean + + init { + hasOneOf = perms.hasOneOf?.mapTo(mutableSetOf(), Util::findRole) + hasNoneOf = perms.hasNoneOf?.mapTo(mutableSetOf(), Util::findRole) + onlyDM = perms.onlyDM + } + + fun isAllowed(message: MessageCreateEvent): Boolean { + if (message.messageAuthor.isBotOwner) { + return true + } + if (onlyDM && !message.isPrivateMessage) { + return false + } + hasOneOf?.let { roles -> + if (!Util.hasOneOf(message.messageAuthor, roles)) return false + } + hasNoneOf?.let { roles -> + if (Util.hasOneOf(message.messageAuthor, roles)) return false + } + return true + } +} diff --git a/src/main/kotlin/moe/kageru/kagebot/config/Config.kt b/src/main/kotlin/moe/kageru/kagebot/config/Config.kt new file mode 100644 index 0000000..e0333be --- /dev/null +++ b/src/main/kotlin/moe/kageru/kagebot/config/Config.kt @@ -0,0 +1,47 @@ +package moe.kageru.kagebot.config + +import moe.kageru.kagebot.Globals +import moe.kageru.kagebot.Globals.api +import moe.kageru.kagebot.command.Command +import moe.kageru.kagebot.features.Features +import moe.kageru.kagebot.features.WelcomeFeature +import java.awt.Color +import kotlin.IllegalArgumentException + +class Config(rawConfig: RawConfig) { + val system: SystemConfig + val localization: Localization + val commands: List + val features: Features + + init { + system = rawConfig.system?.let { + SystemConfig( + it.serverId ?: throw IllegalArgumentException("No [system.server] defined."), + Color.decode(it.color ?: "#1793d0") + ) + } ?: throw IllegalArgumentException("No [system] block in config.") + Globals.server = api.getServerById(system.serverId).orElseThrow() + + localization = rawConfig.localization?.let { + Localization( + permissionDenied = it.permissionDenied + ?: throw IllegalArgumentException("No [localization.permissionDenied] defined"), + redirectedMessage = it.redirectedMessage + ?: throw IllegalArgumentException("No [localization.permissionDenied] defined"), + messageDeleted = it.messageDeleted + ?: throw IllegalArgumentException("No [localization.permissionDenied] defined") + ) + } ?: throw IllegalArgumentException("No [localization] block in config.") + + commands = rawConfig.commands?.let { rawCommands -> + rawCommands.map { Command(it) } + } ?: emptyList() + + features = rawConfig.features?.let { Features(it) } + ?: throw IllegalArgumentException("No [feature] block in config.") + } +} + +class SystemConfig(val serverId: String, val color: Color) +class Localization(val permissionDenied: String, val redirectedMessage: String, val messageDeleted: String) diff --git a/src/main/kotlin/moe/kageru/kagebot/config/RawConfig.kt b/src/main/kotlin/moe/kageru/kagebot/config/RawConfig.kt new file mode 100644 index 0000000..23fd423 --- /dev/null +++ b/src/main/kotlin/moe/kageru/kagebot/config/RawConfig.kt @@ -0,0 +1,50 @@ +package moe.kageru.kagebot.config + +import com.google.gson.annotations.SerializedName +import com.moandjiezana.toml.Toml +import java.io.File + +class RawConfig( + val system: RawSystemConfig?, + val localization: RawLocalization?, + @SerializedName("command") val commands: List?, + @SerializedName("feature") val features: RawFeatures? +) { + companion object { + const val DEFAULT_CONFIG_PATH = "config.toml" + + fun read(path: String = DEFAULT_CONFIG_PATH): RawConfig { + val toml: Toml = Toml().read(run { + val file = File(path) + if (file.isFile) { + return@run file + } + println("Config not found, falling back to defaults...") + File(this::class.java.classLoader.getResource(path)!!.toURI()) + }) + return toml.to(RawConfig::class.java) + } + + } +} + +class RawSystemConfig(val serverId: String?, val color: String?) +class RawLocalization(val permissionDenied: String?, val redirectedMessage: String?, val messageDeleted: String?) +class RawCommand( + val trigger: String?, + val response: String?, + val matchType: String?, + val permissions: RawPermissions?, + @SerializedName("action") val actions: RawMessageActions? +) + +class RawPermissions(val hasOneOf: List?, val hasNoneOf: List?, val onlyDM: Boolean) +class RawMessageActions(val delete: Boolean, val redirect: RawRedirect?) +class RawRedirect(val target: String?, val anonymous: Boolean) +class RawFeatures(val welcome: RawWelcomeFeature?) +class RawWelcomeFeature( + val enabled: Boolean, + val content: Map?, + val fallbackChannel: String?, + val fallbackMessage: String? +) \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kagebot/features/Features.kt b/src/main/kotlin/moe/kageru/kagebot/features/Features.kt new file mode 100644 index 0000000..3927f96 --- /dev/null +++ b/src/main/kotlin/moe/kageru/kagebot/features/Features.kt @@ -0,0 +1,27 @@ +package moe.kageru.kagebot.features + +import moe.kageru.kagebot.MessageUtil +import moe.kageru.kagebot.Util +import moe.kageru.kagebot.config.RawFeatures +import moe.kageru.kagebot.config.RawWelcomeFeature +import org.javacord.api.entity.channel.TextChannel +import org.javacord.api.entity.message.embed.EmbedBuilder + +class Features(rawFeatures: RawFeatures) { + val welcome: WelcomeFeature? = rawFeatures.welcome?.let { WelcomeFeature(it) } +} + +class WelcomeFeature(rawWelcome: RawWelcomeFeature) { + val enabled: Boolean = rawWelcome.enabled + val embed: EmbedBuilder? by lazy { + rawWelcome.content?.let(MessageUtil::mapToEmbed) + } + val fallbackChannel: TextChannel? = rawWelcome.fallbackChannel?.let { + if (rawWelcome.fallbackMessage == null) { + throw IllegalArgumentException("[feature.welcome.fallbackMessage] must not be null if fallbackChannel is defined") + } + Util.findChannel(it) + } + val fallbackMessage: String? = rawWelcome.fallbackMessage + +} diff --git a/src/main/kotlin/moe/kageru/kagebot/main.kt b/src/main/kotlin/moe/kageru/kagebot/main.kt index 9b1026f..7d3f7bb 100644 --- a/src/main/kotlin/moe/kageru/kagebot/main.kt +++ b/src/main/kotlin/moe/kageru/kagebot/main.kt @@ -4,10 +4,10 @@ import moe.kageru.kagebot.Log.log import java.lang.System fun main() { - try { + //try { Kagebot() - } catch (e: Exception) { + /*} catch (e: Exception) { log.warning("An exception occurred in the main thread, exiting.\n${e.stackTrace.joinToString("\n")}") System.exit(1) - } + }*/ } diff --git a/src/main/resources/config.toml b/src/main/resources/config.toml index d46821c..a7f528e 100644 --- a/src/main/resources/config.toml +++ b/src/main/resources/config.toml @@ -8,6 +8,19 @@ permissionDenied = "You do not have permission to use this command." redirectedMessage = "says" messageDeleted = "Your message was deleted because it contained a banned word or phrase." +# If this is enabled, every new user will receive a welcome message. +# If the user has disabled their DMs, the fallbackMessage will be sent in the fallbackChannel instead. +# If no fallback channel or message is specified, no fallback will be sent. +[feature.welcome] +enabled = true +fallbackChannel = 555097559023222825 +fallbackMessage = "@@ I would like to greet you, but I can’t. :(" +# This is a list of pairs where the key is the title and the value the content of the paragraph. +# Do not use empty strings to get empty headings or paragraphs. The discord API rejects those. +[feature.welcome.content] +"Welcome to the Server" = "This is the content of the first paragraph" +"Second paragraph heading" = "Second paragraph content" + [[command]] trigger = "!ping" response = "pong" @@ -38,29 +51,27 @@ trigger = "!restricted" response = "access granted" [command.permissions] hasOneOf = [ - 452034011393425409, - 446668543816106004 + "452034011393425409", + "446668543816106004" ] [[command]] trigger = "!almostUnrestricted" response = "access granted" [command.permissions] -hasNoneOf = [452034011393425409] - - +hasNoneOf = ["452034011393425409"] # redirect every message that starts with !redirect to channel 555097559023222825 [[command]] trigger = "!redirect" response = "redirected" [command.action.redirect] -target = 555097559023222825 +target = "testchannel" # the same, but without the original username [[command]] trigger = "!anonRedirect" response = "redirected" [command.action.redirect] -target = 555097559023222825 +target = "555097559023222825" anonymous = true diff --git a/src/test/kotlin/moe/kageru/kagebot/CommandTest.kt b/src/test/kotlin/moe/kageru/kagebot/CommandTest.kt index f488daf..bab8142 100644 --- a/src/test/kotlin/moe/kageru/kagebot/CommandTest.kt +++ b/src/test/kotlin/moe/kageru/kagebot/CommandTest.kt @@ -6,11 +6,11 @@ import io.kotlintest.specs.StringSpec import io.mockk.every import io.mockk.mockk import io.mockk.verify -import moe.kageru.kagebot.Config.Companion.config import moe.kageru.kagebot.TestUtil.embedToString import moe.kageru.kagebot.TestUtil.messageableAuthor import moe.kageru.kagebot.TestUtil.mockMessage import moe.kageru.kagebot.TestUtil.testMessageSuccess +import moe.kageru.kagebot.config.RawConfig.Companion.config import org.javacord.api.entity.message.embed.EmbedBuilder class CommandTest : StringSpec({ diff --git a/src/test/kotlin/moe/kageru/kagebot/ConfigTest.kt b/src/test/kotlin/moe/kageru/kagebot/ConfigTest.kt index f7fbe89..6bb398d 100644 --- a/src/test/kotlin/moe/kageru/kagebot/ConfigTest.kt +++ b/src/test/kotlin/moe/kageru/kagebot/ConfigTest.kt @@ -2,10 +2,16 @@ package moe.kageru.kagebot import io.kotlintest.shouldNotBe import io.kotlintest.specs.StringSpec +import moe.kageru.kagebot.config.RawConfig class ConfigTest : StringSpec({ + /* "should properly parse default config" { Config.config shouldNotBe null Config.config.commands shouldNotBe null } + */ + "should convert to raw config" { + RawConfig.config shouldNotBe null + } }) \ No newline at end of file diff --git a/src/test/kotlin/moe/kageru/kagebot/FeatureTest.kt b/src/test/kotlin/moe/kageru/kagebot/FeatureTest.kt new file mode 100644 index 0000000..71ccf0c --- /dev/null +++ b/src/test/kotlin/moe/kageru/kagebot/FeatureTest.kt @@ -0,0 +1,45 @@ +package moe.kageru.kagebot + +import io.kotlintest.matchers.string.shouldContain +import io.kotlintest.shouldBe +import io.kotlintest.specs.StringSpec +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import org.javacord.api.entity.message.embed.EmbedBuilder +class FeatureTest +/* +class FeatureTest : StringSpec({ + "should send welcome" { + val sentMessages = mutableListOf() + Kagebot.welcomeUser( + mockk { + every { user } returns mockk { + every { id } returns 123 + every { sendMessage(capture(sentMessages)) } + } + } + ) + sentMessages.size shouldBe 1 + TestUtil.embedToString(sentMessages[0]).let { embed -> + Config.config.features!!.welcome!!.content!!.entries.forEach { (title, content) -> + embed shouldContain title + embed shouldContain content + } + } + } + "should send welcome fallback if DMs are disabled" { + val dm = slot() + val sentMessages = mutableListOf() + TestUtil.prepareServerConfig(sentMessages) + Kagebot.welcomeUser( + mockk { + every { user } returns mockk { + every { id } returns 123 + every { sendMessage(capture(dm)) } + } + } + ) + } +}) + */ \ No newline at end of file diff --git a/src/test/kotlin/moe/kageru/kagebot/TestUtil.kt b/src/test/kotlin/moe/kageru/kagebot/TestUtil.kt index 19f2575..7075fca 100644 --- a/src/test/kotlin/moe/kageru/kagebot/TestUtil.kt +++ b/src/test/kotlin/moe/kageru/kagebot/TestUtil.kt @@ -50,7 +50,7 @@ object TestUtil { val server = mockk() every { server.icon.ifPresent(any()) } just Runs every { server.getTextChannelById(any()) } returns resultMock - Config.server = server + //Config.server = server } fun testMessageSuccess(content: String, result: String) {