From 93a6e92191a2e2d1b67a11e83686fcab268d4731 Mon Sep 17 00:00:00 2001 From: kageru Date: Tue, 23 Jul 2019 21:50:55 +0200 Subject: [PATCH] Add minimal implementation of timeout feature --- build.gradle.kts | 9 +-- src/main/kotlin/moe/kageru/kagebot/Kagebot.kt | 2 + src/main/kotlin/moe/kageru/kagebot/Util.kt | 21 ++++++- .../moe/kageru/kagebot/config/ConfigParser.kt | 2 +- .../moe/kageru/kagebot/config/RawFeatures.kt | 3 +- .../kotlin/moe/kageru/kagebot/cron/CronD.kt | 21 +++++++ .../moe/kageru/kagebot/features/Features.kt | 11 ++-- .../kageru/kagebot/features/TimeoutFeature.kt | 62 +++++++++++++++++++ .../kageru/kagebot/features/WelcomeFeature.kt | 6 +- .../moe/kageru/kagebot/persistence/Dao.kt | 14 +++-- src/main/resources/config.toml | 7 +++ 11 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/moe/kageru/kagebot/cron/CronD.kt create mode 100644 src/main/kotlin/moe/kageru/kagebot/features/TimeoutFeature.kt diff --git a/build.gradle.kts b/build.gradle.kts index bd9d6a6..0e67478 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.3.40" + kotlin("jvm") version "1.3.41" id("com.github.johnrengelman.shadow") version "5.1.0" apply true application } @@ -14,9 +14,9 @@ application { tasks.withType { manifest { attributes( - mapOf( - "Main-Class" to botMainClass - ) + mapOf( + "Main-Class" to botMainClass + ) ) } } @@ -38,6 +38,7 @@ dependencies { implementation(kotlin("stdlib-jdk8")) implementation("org.javacord:javacord:3.0.4") implementation("org.mapdb:mapdb:3.0.7") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC") testImplementation("io.kotlintest:kotlintest-runner-junit5:3.3.2") testImplementation("io.mockk:mockk:1.9.3") diff --git a/src/main/kotlin/moe/kageru/kagebot/Kagebot.kt b/src/main/kotlin/moe/kageru/kagebot/Kagebot.kt index 86b29d1..c8325e0 100644 --- a/src/main/kotlin/moe/kageru/kagebot/Kagebot.kt +++ b/src/main/kotlin/moe/kageru/kagebot/Kagebot.kt @@ -4,6 +4,7 @@ import moe.kageru.kagebot.Util.checked import moe.kageru.kagebot.config.Config import moe.kageru.kagebot.config.ConfigParser import moe.kageru.kagebot.config.RawConfig +import moe.kageru.kagebot.cron.CronD import org.javacord.api.DiscordApiBuilder import org.javacord.api.event.message.MessageCreateEvent import java.io.File @@ -48,5 +49,6 @@ object Kagebot { Log.info("kagebot Mk II running") Globals.api.addMessageCreateListener { checked { it.process() } } Config.features.eventFeatures().forEach { it.register(Globals.api) } + CronD.startAll() } } diff --git a/src/main/kotlin/moe/kageru/kagebot/Util.kt b/src/main/kotlin/moe/kageru/kagebot/Util.kt index bbd8caf..d70f21c 100644 --- a/src/main/kotlin/moe/kageru/kagebot/Util.kt +++ b/src/main/kotlin/moe/kageru/kagebot/Util.kt @@ -22,7 +22,7 @@ object Util { * Mimics the behavior of [Optional.ifPresent], but returns null if the optional is empty, * allowing easier fallback behavior via Kotlin’s ?: operator. */ - private inline fun Optional.ifNotEmpty(op: (T) -> R): R? { + internal inline fun Optional.ifNotEmpty(op: (T) -> R): R? { if (this.isPresent) { return op(this.get()) } @@ -53,6 +53,22 @@ object Util { } } + private fun Optional.toNullable(): T? { + return ifNotEmpty { it } + } + + fun findUser(idOrName: String): User? { + return when { + idOrName.isEntityId() -> server.getMemberById(idOrName).toNullable() + else -> { + when { + idOrName.contains('#') -> server.getMemberByDiscriminatedNameIgnoreCase(idOrName).toNullable() + else -> server.getMembersByName(idOrName).firstOrNull() + } + } + } + } + fun CompletableFuture.failed(): Boolean { try { join() @@ -94,6 +110,7 @@ object Util { op() } catch (e: Exception) { Log.warn("An uncaught exception occurred.\n$e") + Log.warn(e.stackTrace.joinToString("\n")) MessageUtil.sendEmbed( Globals.api.owner.get(), EmbedBuilder() @@ -102,7 +119,7 @@ object Util { .addField( "$e", """``` ${e.stackTrace.joinToString("\n")} - ```""".trimIndent() + ```""".trimIndent().run { applyIf(length > 1800) { substring(1..1800) } } ) ) } diff --git a/src/main/kotlin/moe/kageru/kagebot/config/ConfigParser.kt b/src/main/kotlin/moe/kageru/kagebot/config/ConfigParser.kt index cd0e8c1..d31ea8b 100644 --- a/src/main/kotlin/moe/kageru/kagebot/config/ConfigParser.kt +++ b/src/main/kotlin/moe/kageru/kagebot/config/ConfigParser.kt @@ -31,7 +31,7 @@ object ConfigParser { fun reloadFeatures(rawConfig: RawConfig) { Config.features = rawConfig.features?.let(::Features) - ?: Features(RawFeatures(null)) + ?: Features(RawFeatures(null, null)) } } diff --git a/src/main/kotlin/moe/kageru/kagebot/config/RawFeatures.kt b/src/main/kotlin/moe/kageru/kagebot/config/RawFeatures.kt index 4a00337..75b25a0 100644 --- a/src/main/kotlin/moe/kageru/kagebot/config/RawFeatures.kt +++ b/src/main/kotlin/moe/kageru/kagebot/config/RawFeatures.kt @@ -1,4 +1,5 @@ package moe.kageru.kagebot.config -class RawFeatures(val welcome: RawWelcomeFeature?) +class RawFeatures(val welcome: RawWelcomeFeature?, val timeout: RawTimeoutFeature?) class RawWelcomeFeature(val content: List?, val fallbackChannel: String?, val fallbackMessage: String?) +class RawTimeoutFeature(val role: String?) diff --git a/src/main/kotlin/moe/kageru/kagebot/cron/CronD.kt b/src/main/kotlin/moe/kageru/kagebot/cron/CronD.kt new file mode 100644 index 0000000..58066fb --- /dev/null +++ b/src/main/kotlin/moe/kageru/kagebot/cron/CronD.kt @@ -0,0 +1,21 @@ +package moe.kageru.kagebot.cron + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import moe.kageru.kagebot.config.Config + +object CronD { + fun startAll() { + GlobalScope.launch { + minutely() + } + } + + private suspend fun minutely() { + while (true) { + Config.features.timeout?.checkAndRelease() + delay(60_000) + } + } +} \ 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 index 5c75fa1..ca115f4 100644 --- a/src/main/kotlin/moe/kageru/kagebot/features/Features.kt +++ b/src/main/kotlin/moe/kageru/kagebot/features/Features.kt @@ -7,23 +7,26 @@ class Features( debug: DebugFeature, help: HelpFeature, getConfig: GetConfigFeature, - setConfig: SetConfigFeature + setConfig: SetConfigFeature, + val timeout: TimeoutFeature? ) { constructor(rawFeatures: RawFeatures) : this( rawFeatures.welcome?.let(::WelcomeFeature), DebugFeature(), HelpFeature(), GetConfigFeature(), - SetConfigFeature() + SetConfigFeature(), + rawFeatures.timeout?.let(::TimeoutFeature) ) - private val all = listOf(welcome, debug, help, getConfig, setConfig) + private val all = listOf(welcome, debug, help, getConfig, setConfig, timeout) private val featureMap = mapOf( "help" to help, "debug" to debug, "welcome" to welcome, "getConfig" to getConfig, - "setConfig" to setConfig + "setConfig" to setConfig, + "timeout" to timeout ) fun findByString(feature: String) = featureMap[feature] diff --git a/src/main/kotlin/moe/kageru/kagebot/features/TimeoutFeature.kt b/src/main/kotlin/moe/kageru/kagebot/features/TimeoutFeature.kt new file mode 100644 index 0000000..561f8d8 --- /dev/null +++ b/src/main/kotlin/moe/kageru/kagebot/features/TimeoutFeature.kt @@ -0,0 +1,62 @@ +package moe.kageru.kagebot.features + +import moe.kageru.kagebot.Log +import moe.kageru.kagebot.Util.findRole +import moe.kageru.kagebot.Util.findUser +import moe.kageru.kagebot.Util.ifNotEmpty +import moe.kageru.kagebot.config.Config +import moe.kageru.kagebot.config.RawTimeoutFeature +import moe.kageru.kagebot.persistence.Dao +import org.javacord.api.entity.permission.Role +import org.javacord.api.event.message.MessageCreateEvent +import java.lang.IllegalArgumentException +import java.time.Duration +import java.time.Instant + +class TimeoutFeature(raw: RawTimeoutFeature) : MessageFeature { + private val timeoutRole: Role = raw.role?.let(::findRole) + ?: throw IllegalArgumentException("No timeout role defined") + + override fun handle(message: MessageCreateEvent) { + val (_, target, time) = message.readableMessageContent.split(' ', limit = 3) + findUser(target)?.let { user -> + val oldRoles = user.getRoles(Config.server).map { role -> + user.removeRole(role) + role.id + } + user.addRole(timeoutRole) + val releaseTime = Instant.now().plus(Duration.ofMinutes(time.toLong())).epochSecond + Dao.saveTimeout(releaseTime, listOf(user.id) + oldRoles) + } ?: message.channel.sendMessage("Could not find user $target. Consider using the user ID.") + } + + fun checkAndRelease() { + 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")) + } + user.removeRole(timeoutRole) + } ?: 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) { + 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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/moe/kageru/kagebot/features/WelcomeFeature.kt b/src/main/kotlin/moe/kageru/kagebot/features/WelcomeFeature.kt index b282b4a..d493bca 100644 --- a/src/main/kotlin/moe/kageru/kagebot/features/WelcomeFeature.kt +++ b/src/main/kotlin/moe/kageru/kagebot/features/WelcomeFeature.kt @@ -36,16 +36,16 @@ class WelcomeFeature(rawWelcome: RawWelcomeFeature) : MessageFeature, EventFeatu } } - fun hasFallback(): Boolean = fallbackChannel != null && fallbackMessage != null + private fun hasFallback(): Boolean = fallbackChannel != null && fallbackMessage != null val embed: EmbedBuilder? by lazy { rawWelcome.content?.let(MessageUtil::listToEmbed) } - val fallbackChannel: TextChannel? = rawWelcome.fallbackChannel?.let { + private 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 + private val fallbackMessage: String? = rawWelcome.fallbackMessage } diff --git a/src/main/kotlin/moe/kageru/kagebot/persistence/Dao.kt b/src/main/kotlin/moe/kageru/kagebot/persistence/Dao.kt index 1495cf8..99210b2 100644 --- a/src/main/kotlin/moe/kageru/kagebot/persistence/Dao.kt +++ b/src/main/kotlin/moe/kageru/kagebot/persistence/Dao.kt @@ -5,13 +5,17 @@ import org.mapdb.Serializer object Dao { private val db = DBMaker.fileDB("kagebot.db").fileMmapEnable().closeOnJvmShutdown().make() - private val strings = db.hashMap("main", Serializer.STRING, Serializer.STRING).createOrOpen() + private val prisoners = db.hashMap("timeout", Serializer.LONG, Serializer.LONG_ARRAY).createOrOpen() - fun store(key: String, value: String) { - strings[key] = value + fun saveTimeout(releaseTime: Long, roles: List) { + prisoners[releaseTime] = roles.toLongArray() } - fun get(key: String): String? { - return strings[key] + fun getAllTimeouts() = prisoners.keys + + fun deleteTimeout(releaseTime: Long): LongArray { + val timeout = prisoners[releaseTime]!! + prisoners.remove(releaseTime) + return timeout } } \ No newline at end of file diff --git a/src/main/resources/config.toml b/src/main/resources/config.toml index 0859769..515915c 100644 --- a/src/main/resources/config.toml +++ b/src/main/resources/config.toml @@ -25,6 +25,9 @@ content = [ "5th", "asdasd" ] +[feature.timeout] +role = "timeout" + [[command]] trigger = "!ping" response = "pong" @@ -114,3 +117,7 @@ feature = "getConfig" [[command]] trigger = "!setConfig" feature = "setConfig" + +[[command]] +trigger = "!prison" +feature = "timeout" \ No newline at end of file