Use Either monad for timeout parsing

This commit is contained in:
kageru 2019-11-12 21:13:01 +01:00
parent 50b97fdec7
commit 2c56e1959a
Signed by: kageru
GPG Key ID: 8282A2BEA4ADA3D2
5 changed files with 72 additions and 75 deletions

View File

@ -57,13 +57,13 @@ object Util {
return orElse(null) return orElse(null)
} }
fun findUser(idOrName: String): User? { fun findUser(idOrName: String): Option<User> {
return when { return when {
idOrName.isEntityId() -> server.getMemberById(idOrName).toNullable() idOrName.isEntityId() -> server.getMemberById(idOrName).asOption()
else -> { else -> {
when { when {
idOrName.contains('#') -> server.getMemberByDiscriminatedNameIgnoreCase(idOrName).toNullable() idOrName.contains('#') -> server.getMemberByDiscriminatedNameIgnoreCase(idOrName).asOption()
else -> server.getMembersByName(idOrName).firstOrNull() else -> server.getMembersByName(idOrName).firstOrNull().toOption()
} }
} }
} }

View File

@ -0,0 +1,8 @@
package moe.kageru.kagebot.extensions
import arrow.core.Either
fun <L, R> Either<L, R>.on(op: (R) -> Unit): Either<L, R> {
this.map { op(it) }
return this
}

View File

@ -15,6 +15,7 @@ fun Server.channelById(id: String): Option<ServerTextChannel> = getTextChannelBy
fun Server.channelByName(name: String): ListK<ServerTextChannel> = getTextChannelsByName(name).k() fun Server.channelByName(name: String): ListK<ServerTextChannel> = getTextChannelsByName(name).k()
fun Server.rolesByName(name: String): ListK<Role> = getRolesByNameIgnoreCase(name).k() fun Server.rolesByName(name: String): ListK<Role> = getRolesByNameIgnoreCase(name).k()
fun Server.membersByName(name: String): ListK<User> = getMembersByName(name).toList().k() fun Server.membersByName(name: String): ListK<User> = getMembersByName(name).toList().k()
fun Server.memberById(name: Long): Option<User> = getMemberById(name).asOption()
fun Server.categoriesByName(name: String): ListK<ChannelCategory> = getChannelCategoriesByNameIgnoreCase(name).k() fun Server.categoriesByName(name: String): ListK<ChannelCategory> = getChannelCategoriesByNameIgnoreCase(name).k()
fun User.roles(): ListK<Role> = getRoles(Config.server).k() fun User.roles(): ListK<Role> = getRoles(Config.server).k()

View File

@ -1,19 +1,23 @@
package moe.kageru.kagebot.features package moe.kageru.kagebot.features
import arrow.core.ListK import arrow.core.*
import arrow.core.extensions.list.monad.map import arrow.core.extensions.list.monad.map
import arrow.core.k import arrow.core.extensions.listk.functorFilter.filter
import arrow.syntax.collections.destructured
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.unwrap 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.extensions.memberById
import moe.kageru.kagebot.extensions.on
import moe.kageru.kagebot.extensions.roles
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.entity.user.User
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
@ -22,71 +26,55 @@ class TimeoutFeature(@JsonProperty("role") role: String) : MessageFeature {
private val timeoutRole: Role = findRole(role).unwrap() 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 -> message.readableMessageContent.split(' ', limit = 4).let { args ->
if (args.size < 3) { Either.cond(
message.channel.sendMessage("Error: expected “<command> <user> <time> [<reason>]”. If the name contains spaces, please use the user ID instead.") args.size >= 3,
return { Tuple3(args[1], args[2], args.getOrNull(3)) },
} { "Error: expected “<command> <user> <time> [<reason>]”. If the name contains spaces, please use the user ID instead." }
val time = args[2].toLongOrNull() ).flatMap {
if (time == null) { Tuple3(
message.channel.sendMessage("Error: malformed time") findUser(it.a).orNull()
return ?: return@flatMap "Error: User ${it.a} not found, consider using the user ID".left(),
} it.b.toLongOrNull() ?: return@flatMap "Error: malformed time".left(),
ParsedTimeout(args[1], time, args.getOrNull(3)) it.c
} ).right()
findUser(timeout.target)?.let { user -> }.on { (user, time, _) ->
val oldRoles = user.getRoles(Config.server) applyTimeout(user, time)
.filter { !it.isManaged } }.fold(
.map { role -> { message.channel.sendMessage(it) },
user.removeRole(role) { (user, time, reason) ->
role.id user.sendEmbed {
} addField("Timeout", Config.localization[LocalizationSpec.timeout].replace("@@", "$time"))
user.addRole(timeoutRole) reason?.let { addField("Reason", it) }
val releaseTime = Instant.now().plus(Duration.ofMinutes(timeout.duration)).epochSecond
Dao.saveTimeout(releaseTime, listOf(user.id) + oldRoles)
user.sendEmbed {
addField(
"Timeout",
Config.localization[LocalizationSpec.timeout].replace("@@", timeout.duration.toString())
)
timeout.reason?.let {
addField("Reason", it)
}
}
Log.info("Removed roles ${oldRoles.joinToString()} from user ${user.discriminatedName}")
} ?: message.channel.sendMessage("Could not find user ${timeout.target}. Consider using the user ID.")
}
fun checkAndRelease() {
val now = Instant.now().epochSecond
Dao.getAllTimeouts()
.filter { releaseTime -> now > releaseTime }
.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()}")
} }
) }
} )
}
}
class UserInTimeout(private val id: Long, private val roles: ListK<Long>) {
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.k())
} }
} }
}
class ParsedTimeout(val target: String, val duration: Long, val reason: String?) private fun applyTimeout(user: User, time: Long) {
val oldRoles = user.roles()
.filter { !it.isManaged }
.onEach { user.removeRole(it) }
.map { it.id }
user.addRole(timeoutRole)
val releaseTime = Instant.now().plus(Duration.ofMinutes(time)).epochSecond
Dao.saveTimeout(releaseTime, user.id, oldRoles)
Log.info("Removed roles ${oldRoles.joinToString()} from user ${user.discriminatedName}")
}
fun checkAndRelease(): Unit = Dao.getAllTimeouts()
.filter { releaseTime -> Instant.now().epochSecond > releaseTime }
.map { Dao.deleteTimeout(it) }
.map { it.destructured() }
.forEach { (userId, roleIds) ->
Config.server.memberById(userId).fold(
{ Log.warn("Tried to free user $userId, but couldn’t find them on the server anymore") },
{ user ->
roleIds.forEach { findRole("$it").map(user::addRole) }
user.removeRole(timeoutRole)
Log.info("Lifted timeout from user ${user.discriminatedName}. Stored roles ${roleIds.joinToString()}")
}
)
}
}

View File

@ -10,8 +10,8 @@ object Dao {
private val commands = db.hashMap("commands", Serializer.STRING, Serializer.INTEGER).createOrOpen() private val commands = db.hashMap("commands", Serializer.STRING, Serializer.INTEGER).createOrOpen()
private val tempVcs = db.hashSet("vcs", Serializer.STRING).createOrOpen() private val tempVcs = db.hashSet("vcs", Serializer.STRING).createOrOpen()
fun saveTimeout(releaseTime: Long, roles: List<Long>) { fun saveTimeout(releaseTime: Long, user: Long, roles: List<Long>) {
prisoners[releaseTime] = roles.toLongArray() prisoners[releaseTime] = (listOf(user) + roles).toLongArray()
} }
fun setCommandCounter(count: Int) { fun setCommandCounter(count: Int) {
@ -24,10 +24,10 @@ object Dao {
fun getAllTimeouts() = prisoners.keys.k() fun getAllTimeouts() = prisoners.keys.k()
fun deleteTimeout(releaseTime: Long): LongArray { fun deleteTimeout(releaseTime: Long): List<Long> {
val timeout = prisoners[releaseTime]!! val timeout = prisoners[releaseTime]!!
prisoners.remove(releaseTime) prisoners.remove(releaseTime)
return timeout return timeout.toList()
} }
fun isTemporaryVC(channel: String): Boolean { fun isTemporaryVC(channel: String): Boolean {