No longer throw exceptions for role/channel queries

This commit is contained in:
kageru 2019-11-12 14:02:31 +01:00
parent 3d813384e2
commit 50b97fdec7
Signed by: kageru
GPG Key ID: 8282A2BEA4ADA3D2
8 changed files with 66 additions and 49 deletions

View File

@ -1,7 +1,9 @@
package moe.kageru.kagebot 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.core.extensions.list.foldable.find
import arrow.typeclasses.Monad
import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.config.Config
import moe.kageru.kagebot.extensions.* import moe.kageru.kagebot.extensions.*
import moe.kageru.kagebot.config.Config.server import moe.kageru.kagebot.config.Config.server
@ -37,21 +39,17 @@ object Util {
private val channelIdRegex = Regex("\\d{18}") private val channelIdRegex = Regex("\\d{18}")
private fun String.isEntityId() = channelIdRegex.matches(this) private fun String.isEntityId() = channelIdRegex.matches(this)
@Throws(IllegalArgumentException::class) fun findRole(idOrName: String): Either<String, Role> {
fun findRole(idOrName: String): Role {
return when { return when {
idOrName.isEntityId() -> server.getRoleById(idOrName).ifNotEmpty { it } idOrName.isEntityId() -> server.getRoleById(idOrName).asOption().toEither { 0 }
?: throw IllegalArgumentException("Role $idOrName not found.") else -> server.rolesByName(idOrName).getOnly()
else -> server.getRolesByNameIgnoreCase(idOrName).getOnlyElementOrError(idOrName) }.mapLeft { "Found $it results, expected 1" }
}
} }
private inline fun <reified T> List<T>.getOnlyElementOrError(identifier: String): T { private fun <T> ListK<T>.getOnly(): Either<Int, T> {
val className = T::class.simpleName!!
return when (size) { return when (size) {
0 -> throw IllegalArgumentException("$className $identifier not found.") 1 -> Either.right(first())
1 -> first() else -> Either.left(size)
else -> throw IllegalArgumentException("More than one ${className.toLowerCase()} found with name $identifier. Please specify the role ID instead")
} }
} }
@ -71,6 +69,16 @@ object Util {
} }
} }
fun <T> CompletableFuture<T>.asOption(): Option<T> {
return try {
Option.just(join())
} catch (e: CompletionException) {
Option.empty()
}
}
fun <T> Either<*, T>.unwrap(): T = this.getOrElse { error("Attempted to unwrap Either.left") }
fun <T> CompletableFuture<T>.failed(): Boolean { fun <T> CompletableFuture<T>.failed(): Boolean {
try { try {
join() join()
@ -87,20 +95,15 @@ object Util {
return isCompletedExceptionally return isCompletedExceptionally
} }
@Throws(IllegalArgumentException::class) fun findChannel(idOrName: String): Either<String, TextChannel> {
fun findChannel(idOrName: String): TextChannel {
return when { return when {
idOrName.isEntityId() -> server.getTextChannelById(idOrName).ifNotEmpty { it } idOrName.isEntityId() -> server.getTextChannelById(idOrName).asOption().toEither { "Channel $idOrName not found" }
?: throw IllegalArgumentException("Channel ID $idOrName not found.") idOrName.startsWith('@') -> Globals.api.getCachedUserByDiscriminatedName(idOrName.removePrefix("@")).asOption()
else -> if (idOrName.startsWith('@')) { .toEither { "User $idOrName not found" }
Globals.api.getCachedUserByDiscriminatedName(idOrName.removePrefix("@")).ifNotEmpty { user -> .flatMap { user ->
user.openPrivateChannel().joinOr { user.openPrivateChannel().asOption().toEither { "Can’t DM user $idOrName" }
throw IllegalArgumentException("Could not open private channel with user $idOrName for redirection.") }
} else -> server.channelByName(idOrName).getOnly().mapLeft { "Found $it channels for $idOrName, expected 1" }
} ?: throw IllegalArgumentException("Can’t find user $idOrName for redirection.")
} else {
server.getTextChannelsByName(idOrName).getOnlyElementOrError(idOrName)
}
} }
} }

View File

@ -5,13 +5,14 @@ import moe.kageru.kagebot.MessageUtil
import moe.kageru.kagebot.Util import moe.kageru.kagebot.Util
import moe.kageru.kagebot.Util.applyIf import moe.kageru.kagebot.Util.applyIf
import moe.kageru.kagebot.Util.failed import moe.kageru.kagebot.Util.failed
import moe.kageru.kagebot.Util.unwrap
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 org.javacord.api.entity.channel.TextChannel import org.javacord.api.entity.channel.TextChannel
import org.javacord.api.event.message.MessageCreateEvent import org.javacord.api.event.message.MessageCreateEvent
class MessageRedirect(target: String, private val anonymous: Boolean = false) { 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) { fun execute(message: MessageCreateEvent, command: Command) {
val embed = MessageUtil.withEmbed { val embed = MessageUtil.withEmbed {

View File

@ -2,6 +2,7 @@ package moe.kageru.kagebot.command
import arrow.core.Option import arrow.core.Option
import moe.kageru.kagebot.Util import moe.kageru.kagebot.Util
import moe.kageru.kagebot.Util.unwrap
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
@ -10,14 +11,18 @@ class Permissions(
hasNoneOf: List<String>?, hasNoneOf: List<String>?,
private val onlyDM: Boolean = false private val onlyDM: Boolean = false
) { ) {
private val hasOneOf: Option<Set<Role>> = Option.fromNullable(hasOneOf?.mapTo(mutableSetOf(), Util::findRole)) private val hasOneOf: Option<Set<Role>> = resolveRoles(hasOneOf)
private val hasNoneOf: Option<Set<Role>> = Option.fromNullable(hasNoneOf?.mapTo(mutableSetOf(), Util::findRole)) private val hasNoneOf: Option<Set<Role>> = resolveRoles(hasNoneOf)
private fun resolveRoles(hasOneOf: List<String>?): Option<Set<Role>> {
return Option.fromNullable(hasOneOf?.mapTo(mutableSetOf(), { Util.findRole(it).unwrap() }))
}
fun isAllowed(message: MessageCreateEvent): Boolean = when { fun isAllowed(message: MessageCreateEvent): Boolean = when {
message.messageAuthor.isBotOwner -> true message.messageAuthor.isBotOwner -> true
onlyDM && !message.isPrivateMessage -> false onlyDM && !message.isPrivateMessage -> false
// returns true if the Option is empty (case for no restrictions) // returns true if the Option is empty (case for no restrictions)
else -> hasOneOf.forall { Util.hasOneOf(message.messageAuthor, it) } else -> hasOneOf.forall { Util.hasOneOf(message.messageAuthor, it) }
&& hasNoneOf.forall { !Util.hasOneOf(message.messageAuthor, it) } && hasNoneOf.forall { !Util.hasOneOf(message.messageAuthor, it) }
} }
} }

View File

@ -4,10 +4,11 @@ import com.fasterxml.jackson.annotation.JsonProperty
import moe.kageru.kagebot.Log import moe.kageru.kagebot.Log
import moe.kageru.kagebot.Util import moe.kageru.kagebot.Util
import moe.kageru.kagebot.Util.getUser import moe.kageru.kagebot.Util.getUser
import moe.kageru.kagebot.Util.unwrap
import org.javacord.api.event.message.MessageCreateEvent import org.javacord.api.event.message.MessageCreateEvent
class RoleAssignment(@JsonProperty("role") role: String) { class RoleAssignment(@JsonProperty("role") role: String) {
private val role = Util.findRole(role) private val role = Util.findRole(role).unwrap()
fun assign(message: MessageCreateEvent) = fun assign(message: MessageCreateEvent) =
message.getUser()?.addRole(role, "Requested via command.") message.getUser()?.addRole(role, "Requested via command.")

View File

@ -1,11 +1,15 @@
package moe.kageru.kagebot.features 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 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.asOption
import moe.kageru.kagebot.Util.findRole import moe.kageru.kagebot.Util.findRole
import moe.kageru.kagebot.Util.findUser 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.Config
import moe.kageru.kagebot.config.LocalizationSpec import moe.kageru.kagebot.config.LocalizationSpec
import moe.kageru.kagebot.persistence.Dao import moe.kageru.kagebot.persistence.Dao
@ -15,7 +19,7 @@ import java.time.Duration
import java.time.Instant import java.time.Instant
class TimeoutFeature(@JsonProperty("role") role: String) : MessageFeature { 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) { override fun handle(message: MessageCreateEvent) {
val timeout = message.readableMessageContent.split(' ', limit = 4).let { args -> 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 val now = Instant.now().epochSecond
Dao.getAllTimeouts() Dao.getAllTimeouts()
.filter { releaseTime -> now > releaseTime } .filter { releaseTime -> now > releaseTime }
.map { .map { Dao.deleteTimeout(it) }
Dao.deleteTimeout(it).let { rawIds -> .map { UserInTimeout.ofLongs(it).toPair() }
UserInTimeout.ofLongs(rawIds).toPair() .forEach { (userId, roleIds) ->
} Config.server.getMemberById(userId).asOption().fold(
}.forEach { (userId, roleIds) -> { Log.warn("Tried to free user $userId, but couldn’t find them on the server anymore") }, { user ->
Config.server.getMemberById(userId).ifNotEmpty { user -> roleIds.forEach { roleId ->
roleIds.forEach { roleId -> findRole("$roleId").map(user::addRole)
user.addRole(findRole("$roleId")) }
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<Long>) { class UserInTimeout(private val id: Long, private val roles: ListK<Long>) {
fun toPair() = Pair(id, roles) fun toPair() = Pair(id, roles)
companion object { companion object {
fun ofLongs(longs: LongArray): UserInTimeout = longs.run { fun ofLongs(longs: LongArray): UserInTimeout = longs.run {
val userId = first() val userId = first()
val roles = if (size > 1) slice(1 until size) else emptyList() val roles = if (size > 1) slice(1 until size) else emptyList()
return UserInTimeout(userId, roles) return UserInTimeout(userId, roles.k())
} }
} }
} }

View File

@ -5,6 +5,7 @@ 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.Util.unwrap
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
@ -45,10 +46,10 @@ class WelcomeFeature(
private fun hasFallback(): Boolean = fallbackChannel != null && fallbackMessage != null private fun hasFallback(): Boolean = fallbackChannel != null && fallbackMessage != null
private val fallbackChannel: TextChannel? = fallbackChannel?.let { private val fallbackChannel: TextChannel? = fallbackChannel?.let { channel ->
requireNotNull(fallbackMessage) { requireNotNull(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(channel).unwrap()
} }
} }

View File

@ -1,5 +1,6 @@
package moe.kageru.kagebot.persistence package moe.kageru.kagebot.persistence
import arrow.core.k
import org.mapdb.DBMaker import org.mapdb.DBMaker
import org.mapdb.Serializer import org.mapdb.Serializer
@ -21,7 +22,7 @@ object Dao {
fun close() = db.close() fun close() = db.close()
fun getAllTimeouts() = prisoners.keys fun getAllTimeouts() = prisoners.keys.k()
fun deleteTimeout(releaseTime: Long): LongArray { fun deleteTimeout(releaseTime: Long): LongArray {
val timeout = prisoners[releaseTime]!! val timeout = prisoners[releaseTime]!!

View File

@ -15,6 +15,7 @@ import moe.kageru.kagebot.TestUtil.prepareTestEnvironment
import moe.kageru.kagebot.TestUtil.testMessageSuccess import moe.kageru.kagebot.TestUtil.testMessageSuccess
import moe.kageru.kagebot.TestUtil.withCommands import moe.kageru.kagebot.TestUtil.withCommands
import moe.kageru.kagebot.Util import moe.kageru.kagebot.Util
import moe.kageru.kagebot.Util.unwrap
import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.config.Config
import moe.kageru.kagebot.persistence.Dao import moe.kageru.kagebot.persistence.Dao
import org.javacord.api.entity.message.embed.EmbedBuilder import org.javacord.api.entity.message.embed.EmbedBuilder
@ -275,7 +276,7 @@ class CommandTest : StringSpec({
} }
every { Config.server.getMemberById(1) } returns Optional.of(user) every { Config.server.getMemberById(1) } returns Optional.of(user)
mockMessage("!assign").process() mockMessage("!assign").process()
roles shouldBe mutableListOf(Util.findRole("testrole")) roles shouldBe mutableListOf(Util.findRole("testrole").unwrap())
} }
} }
"should create VC" { "should create VC" {