Add minimal implementation of timeout feature

This commit is contained in:
kageru 2019-07-23 21:50:55 +02:00
parent 74c2f643f9
commit 93a6e92191
Signed by: kageru
GPG Key ID: 8282A2BEA4ADA3D2
11 changed files with 138 additions and 20 deletions

View File

@ -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<Jar> {
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")

View File

@ -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()
}
}

View File

@ -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 Kotlins ?: operator.
*/
private inline fun <T, R> Optional<T>.ifNotEmpty(op: (T) -> R): R? {
internal inline fun <T, R> Optional<T>.ifNotEmpty(op: (T) -> R): R? {
if (this.isPresent) {
return op(this.get())
}
@ -53,6 +53,22 @@ object Util {
}
}
private fun <T> Optional<T>.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 <T> CompletableFuture<T>.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) } }
)
)
}

View File

@ -31,7 +31,7 @@ object ConfigParser {
fun reloadFeatures(rawConfig: RawConfig) {
Config.features = rawConfig.features?.let(::Features)
?: Features(RawFeatures(null))
?: Features(RawFeatures(null, null))
}
}

View File

@ -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<String>?, val fallbackChannel: String?, val fallbackMessage: String?)
class RawTimeoutFeature(val role: String?)

View File

@ -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)
}
}
}

View File

@ -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]

View File

@ -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<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)
}
}
}

View File

@ -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
}

View File

@ -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<Long>) {
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
}
}

View File

@ -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"