From 50b97fdec7073a7dd20777ced696b85b4e32a5c7 Mon Sep 17 00:00:00 2001 From: kageru Date: Tue, 12 Nov 2019 14:02:31 +0100 Subject: [PATCH] No longer throw exceptions for role/channel queries --- src/main/kotlin/moe/kageru/kagebot/Util.kt | 53 ++++++++++--------- .../kageru/kagebot/command/MessageRedirect.kt | 3 +- .../moe/kageru/kagebot/command/Permissions.kt | 11 ++-- .../kageru/kagebot/command/RoleAssignment.kt | 3 +- .../kageru/kagebot/features/TimeoutFeature.kt | 34 ++++++------ .../kageru/kagebot/features/WelcomeFeature.kt | 5 +- .../moe/kageru/kagebot/persistence/Dao.kt | 3 +- .../moe/kageru/kagebot/command/CommandTest.kt | 3 +- 8 files changed, 66 insertions(+), 49 deletions(-) diff --git a/src/main/kotlin/moe/kageru/kagebot/Util.kt b/src/main/kotlin/moe/kageru/kagebot/Util.kt index 36ab9a8..3e0783c 100644 --- a/src/main/kotlin/moe/kageru/kagebot/Util.kt +++ b/src/main/kotlin/moe/kageru/kagebot/Util.kt @@ -1,7 +1,9 @@ package moe.kageru.kagebot -import arrow.core.Option +import arrow.core.* +import arrow.core.extensions.either.monad.flatMap import arrow.core.extensions.list.foldable.find +import arrow.typeclasses.Monad import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.extensions.* import moe.kageru.kagebot.config.Config.server @@ -37,21 +39,17 @@ object Util { private val channelIdRegex = Regex("\\d{18}") private fun String.isEntityId() = channelIdRegex.matches(this) - @Throws(IllegalArgumentException::class) - fun findRole(idOrName: String): Role { + fun findRole(idOrName: String): Either { return when { - idOrName.isEntityId() -> server.getRoleById(idOrName).ifNotEmpty { it } - ?: throw IllegalArgumentException("Role $idOrName not found.") - else -> server.getRolesByNameIgnoreCase(idOrName).getOnlyElementOrError(idOrName) - } + idOrName.isEntityId() -> server.getRoleById(idOrName).asOption().toEither { 0 } + else -> server.rolesByName(idOrName).getOnly() + }.mapLeft { "Found $it results, expected 1" } } - private inline fun List.getOnlyElementOrError(identifier: String): T { - val className = T::class.simpleName!! + private fun ListK.getOnly(): Either { return when (size) { - 0 -> throw IllegalArgumentException("$className $identifier not found.") - 1 -> first() - else -> throw IllegalArgumentException("More than one ${className.toLowerCase()} found with name $identifier. Please specify the role ID instead") + 1 -> Either.right(first()) + else -> Either.left(size) } } @@ -71,6 +69,16 @@ object Util { } } + fun CompletableFuture.asOption(): Option { + return try { + Option.just(join()) + } catch (e: CompletionException) { + Option.empty() + } + } + + fun Either<*, T>.unwrap(): T = this.getOrElse { error("Attempted to unwrap Either.left") } + fun CompletableFuture.failed(): Boolean { try { join() @@ -87,20 +95,15 @@ object Util { return isCompletedExceptionally } - @Throws(IllegalArgumentException::class) - fun findChannel(idOrName: String): TextChannel { + fun findChannel(idOrName: String): Either { return when { - idOrName.isEntityId() -> server.getTextChannelById(idOrName).ifNotEmpty { it } - ?: throw IllegalArgumentException("Channel ID $idOrName not found.") - else -> if (idOrName.startsWith('@')) { - Globals.api.getCachedUserByDiscriminatedName(idOrName.removePrefix("@")).ifNotEmpty { user -> - user.openPrivateChannel().joinOr { - 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).getOnlyElementOrError(idOrName) - } + idOrName.isEntityId() -> server.getTextChannelById(idOrName).asOption().toEither { "Channel $idOrName not found" } + idOrName.startsWith('@') -> Globals.api.getCachedUserByDiscriminatedName(idOrName.removePrefix("@")).asOption() + .toEither { "User $idOrName not found" } + .flatMap { user -> + user.openPrivateChannel().asOption().toEither { "Can’t DM user $idOrName" } + } + else -> server.channelByName(idOrName).getOnly().mapLeft { "Found $it channels for $idOrName, expected 1" } } } diff --git a/src/main/kotlin/moe/kageru/kagebot/command/MessageRedirect.kt b/src/main/kotlin/moe/kageru/kagebot/command/MessageRedirect.kt index 2da83fd..7090fc2 100644 --- a/src/main/kotlin/moe/kageru/kagebot/command/MessageRedirect.kt +++ b/src/main/kotlin/moe/kageru/kagebot/command/MessageRedirect.kt @@ -5,13 +5,14 @@ import moe.kageru.kagebot.MessageUtil import moe.kageru.kagebot.Util import moe.kageru.kagebot.Util.applyIf import moe.kageru.kagebot.Util.failed +import moe.kageru.kagebot.Util.unwrap import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.config.LocalizationSpec import org.javacord.api.entity.channel.TextChannel import org.javacord.api.event.message.MessageCreateEvent class MessageRedirect(target: String, private val anonymous: Boolean = false) { - private val targetChannel: TextChannel = Util.findChannel(target) + private val targetChannel: TextChannel = Util.findChannel(target).unwrap() fun execute(message: MessageCreateEvent, command: Command) { val embed = MessageUtil.withEmbed { diff --git a/src/main/kotlin/moe/kageru/kagebot/command/Permissions.kt b/src/main/kotlin/moe/kageru/kagebot/command/Permissions.kt index 915dd6b..ab563e1 100644 --- a/src/main/kotlin/moe/kageru/kagebot/command/Permissions.kt +++ b/src/main/kotlin/moe/kageru/kagebot/command/Permissions.kt @@ -2,6 +2,7 @@ package moe.kageru.kagebot.command import arrow.core.Option import moe.kageru.kagebot.Util +import moe.kageru.kagebot.Util.unwrap import org.javacord.api.entity.permission.Role import org.javacord.api.event.message.MessageCreateEvent @@ -10,14 +11,18 @@ class Permissions( hasNoneOf: List?, private val onlyDM: Boolean = false ) { - private val hasOneOf: Option> = Option.fromNullable(hasOneOf?.mapTo(mutableSetOf(), Util::findRole)) - private val hasNoneOf: Option> = Option.fromNullable(hasNoneOf?.mapTo(mutableSetOf(), Util::findRole)) + private val hasOneOf: Option> = resolveRoles(hasOneOf) + private val hasNoneOf: Option> = resolveRoles(hasNoneOf) + + private fun resolveRoles(hasOneOf: List?): Option> { + return Option.fromNullable(hasOneOf?.mapTo(mutableSetOf(), { Util.findRole(it).unwrap() })) + } fun isAllowed(message: MessageCreateEvent): Boolean = when { message.messageAuthor.isBotOwner -> true onlyDM && !message.isPrivateMessage -> false // returns true if the Option is empty (case for no restrictions) else -> hasOneOf.forall { Util.hasOneOf(message.messageAuthor, it) } - && hasNoneOf.forall { !Util.hasOneOf(message.messageAuthor, it) } + && hasNoneOf.forall { !Util.hasOneOf(message.messageAuthor, it) } } } diff --git a/src/main/kotlin/moe/kageru/kagebot/command/RoleAssignment.kt b/src/main/kotlin/moe/kageru/kagebot/command/RoleAssignment.kt index c3d28a7..8c85902 100644 --- a/src/main/kotlin/moe/kageru/kagebot/command/RoleAssignment.kt +++ b/src/main/kotlin/moe/kageru/kagebot/command/RoleAssignment.kt @@ -4,10 +4,11 @@ import com.fasterxml.jackson.annotation.JsonProperty import moe.kageru.kagebot.Log import moe.kageru.kagebot.Util import moe.kageru.kagebot.Util.getUser +import moe.kageru.kagebot.Util.unwrap import org.javacord.api.event.message.MessageCreateEvent class RoleAssignment(@JsonProperty("role") role: String) { - private val role = Util.findRole(role) + private val role = Util.findRole(role).unwrap() fun assign(message: MessageCreateEvent) = message.getUser()?.addRole(role, "Requested via command.") diff --git a/src/main/kotlin/moe/kageru/kagebot/features/TimeoutFeature.kt b/src/main/kotlin/moe/kageru/kagebot/features/TimeoutFeature.kt index 6e14443..013123e 100644 --- a/src/main/kotlin/moe/kageru/kagebot/features/TimeoutFeature.kt +++ b/src/main/kotlin/moe/kageru/kagebot/features/TimeoutFeature.kt @@ -1,11 +1,15 @@ package moe.kageru.kagebot.features +import arrow.core.ListK +import arrow.core.extensions.list.monad.map +import arrow.core.k import com.fasterxml.jackson.annotation.JsonProperty import moe.kageru.kagebot.Log import moe.kageru.kagebot.MessageUtil.sendEmbed +import moe.kageru.kagebot.Util.asOption import moe.kageru.kagebot.Util.findRole import moe.kageru.kagebot.Util.findUser -import moe.kageru.kagebot.Util.ifNotEmpty +import moe.kageru.kagebot.Util.unwrap import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.config.LocalizationSpec import moe.kageru.kagebot.persistence.Dao @@ -15,7 +19,7 @@ import java.time.Duration import java.time.Instant class TimeoutFeature(@JsonProperty("role") role: String) : MessageFeature { - private val timeoutRole: Role = findRole(role) + private val timeoutRole: Role = findRole(role).unwrap() override fun handle(message: MessageCreateEvent) { val timeout = message.readableMessageContent.split(' ', limit = 4).let { args -> @@ -57,30 +61,30 @@ class TimeoutFeature(@JsonProperty("role") role: String) : MessageFeature { val now = Instant.now().epochSecond Dao.getAllTimeouts() .filter { releaseTime -> now > releaseTime } - .map { - Dao.deleteTimeout(it).let { rawIds -> - UserInTimeout.ofLongs(rawIds).toPair() - } - }.forEach { (userId, roleIds) -> - Config.server.getMemberById(userId).ifNotEmpty { user -> - roleIds.forEach { roleId -> - user.addRole(findRole("$roleId")) + .map { Dao.deleteTimeout(it) } + .map { UserInTimeout.ofLongs(it).toPair() } + .forEach { (userId, roleIds) -> + Config.server.getMemberById(userId).asOption().fold( + { Log.warn("Tried to free user $userId, but couldn’t find them on the server anymore") }, { user -> + roleIds.forEach { roleId -> + findRole("$roleId").map(user::addRole) + } + user.removeRole(timeoutRole) + Log.info("Lifted timeout from user ${user.discriminatedName}. Stored roles ${roleIds.joinToString()}") } - user.removeRole(timeoutRole) - Log.info("Lifted timeout from user ${user.discriminatedName}. Stored roles ${roleIds.joinToString()}") - } ?: Log.warn("Tried to free user $userId, but couldn’t find them on the server anymore") + ) } } } -class UserInTimeout(private val id: Long, private val roles: List) { +class UserInTimeout(private val id: Long, private val roles: ListK) { fun toPair() = Pair(id, roles) companion object { fun ofLongs(longs: LongArray): UserInTimeout = longs.run { val userId = first() val roles = if (size > 1) slice(1 until size) else emptyList() - return UserInTimeout(userId, roles) + return UserInTimeout(userId, roles.k()) } } } diff --git a/src/main/kotlin/moe/kageru/kagebot/features/WelcomeFeature.kt b/src/main/kotlin/moe/kageru/kagebot/features/WelcomeFeature.kt index f51c035..913eec6 100644 --- a/src/main/kotlin/moe/kageru/kagebot/features/WelcomeFeature.kt +++ b/src/main/kotlin/moe/kageru/kagebot/features/WelcomeFeature.kt @@ -5,6 +5,7 @@ import moe.kageru.kagebot.MessageUtil import moe.kageru.kagebot.Util import moe.kageru.kagebot.Util.checked import moe.kageru.kagebot.Util.failed +import moe.kageru.kagebot.Util.unwrap import org.javacord.api.DiscordApi import org.javacord.api.entity.channel.TextChannel import org.javacord.api.entity.message.embed.EmbedBuilder @@ -45,10 +46,10 @@ class WelcomeFeature( private fun hasFallback(): Boolean = fallbackChannel != null && fallbackMessage != null - private val fallbackChannel: TextChannel? = fallbackChannel?.let { + private val fallbackChannel: TextChannel? = fallbackChannel?.let { channel -> requireNotNull(fallbackMessage) { "[feature.welcome.fallbackMessage] must not be null if fallbackChannel is defined" } - Util.findChannel(it) + Util.findChannel(channel).unwrap() } } diff --git a/src/main/kotlin/moe/kageru/kagebot/persistence/Dao.kt b/src/main/kotlin/moe/kageru/kagebot/persistence/Dao.kt index 520ddaa..322abf4 100644 --- a/src/main/kotlin/moe/kageru/kagebot/persistence/Dao.kt +++ b/src/main/kotlin/moe/kageru/kagebot/persistence/Dao.kt @@ -1,5 +1,6 @@ package moe.kageru.kagebot.persistence +import arrow.core.k import org.mapdb.DBMaker import org.mapdb.Serializer @@ -21,7 +22,7 @@ object Dao { fun close() = db.close() - fun getAllTimeouts() = prisoners.keys + fun getAllTimeouts() = prisoners.keys.k() fun deleteTimeout(releaseTime: Long): LongArray { val timeout = prisoners[releaseTime]!! diff --git a/src/test/kotlin/moe/kageru/kagebot/command/CommandTest.kt b/src/test/kotlin/moe/kageru/kagebot/command/CommandTest.kt index 03fc7f8..81c95fa 100644 --- a/src/test/kotlin/moe/kageru/kagebot/command/CommandTest.kt +++ b/src/test/kotlin/moe/kageru/kagebot/command/CommandTest.kt @@ -15,6 +15,7 @@ import moe.kageru.kagebot.TestUtil.prepareTestEnvironment import moe.kageru.kagebot.TestUtil.testMessageSuccess import moe.kageru.kagebot.TestUtil.withCommands import moe.kageru.kagebot.Util +import moe.kageru.kagebot.Util.unwrap import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.persistence.Dao import org.javacord.api.entity.message.embed.EmbedBuilder @@ -275,7 +276,7 @@ class CommandTest : StringSpec({ } every { Config.server.getMemberById(1) } returns Optional.of(user) mockMessage("!assign").process() - roles shouldBe mutableListOf(Util.findRole("testrole")) + roles shouldBe mutableListOf(Util.findRole("testrole").unwrap()) } } "should create VC" {