Add minimal implementation of timeout feature
This commit is contained in:
parent
74c2f643f9
commit
93a6e92191
|
@ -1,7 +1,7 @@
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.3.40"
|
kotlin("jvm") version "1.3.41"
|
||||||
id("com.github.johnrengelman.shadow") version "5.1.0" apply true
|
id("com.github.johnrengelman.shadow") version "5.1.0" apply true
|
||||||
application
|
application
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,9 @@ application {
|
||||||
tasks.withType<Jar> {
|
tasks.withType<Jar> {
|
||||||
manifest {
|
manifest {
|
||||||
attributes(
|
attributes(
|
||||||
mapOf(
|
mapOf(
|
||||||
"Main-Class" to botMainClass
|
"Main-Class" to botMainClass
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ dependencies {
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation("org.javacord:javacord:3.0.4")
|
implementation("org.javacord:javacord:3.0.4")
|
||||||
implementation("org.mapdb:mapdb:3.0.7")
|
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.kotlintest:kotlintest-runner-junit5:3.3.2")
|
||||||
testImplementation("io.mockk:mockk:1.9.3")
|
testImplementation("io.mockk:mockk:1.9.3")
|
||||||
|
|
|
@ -4,6 +4,7 @@ import moe.kageru.kagebot.Util.checked
|
||||||
import moe.kageru.kagebot.config.Config
|
import moe.kageru.kagebot.config.Config
|
||||||
import moe.kageru.kagebot.config.ConfigParser
|
import moe.kageru.kagebot.config.ConfigParser
|
||||||
import moe.kageru.kagebot.config.RawConfig
|
import moe.kageru.kagebot.config.RawConfig
|
||||||
|
import moe.kageru.kagebot.cron.CronD
|
||||||
import org.javacord.api.DiscordApiBuilder
|
import org.javacord.api.DiscordApiBuilder
|
||||||
import org.javacord.api.event.message.MessageCreateEvent
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -48,5 +49,6 @@ object Kagebot {
|
||||||
Log.info("kagebot Mk II running")
|
Log.info("kagebot Mk II running")
|
||||||
Globals.api.addMessageCreateListener { checked { it.process() } }
|
Globals.api.addMessageCreateListener { checked { it.process() } }
|
||||||
Config.features.eventFeatures().forEach { it.register(Globals.api) }
|
Config.features.eventFeatures().forEach { it.register(Globals.api) }
|
||||||
|
CronD.startAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ object Util {
|
||||||
* Mimics the behavior of [Optional.ifPresent], but returns null if the optional is empty,
|
* Mimics the behavior of [Optional.ifPresent], but returns null if the optional is empty,
|
||||||
* allowing easier fallback behavior via Kotlin’s ?: operator.
|
* allowing easier fallback behavior via Kotlin’s ?: 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) {
|
if (this.isPresent) {
|
||||||
return op(this.get())
|
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 {
|
fun <T> CompletableFuture<T>.failed(): Boolean {
|
||||||
try {
|
try {
|
||||||
join()
|
join()
|
||||||
|
@ -94,6 +110,7 @@ object Util {
|
||||||
op()
|
op()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.warn("An uncaught exception occurred.\n$e")
|
Log.warn("An uncaught exception occurred.\n$e")
|
||||||
|
Log.warn(e.stackTrace.joinToString("\n"))
|
||||||
MessageUtil.sendEmbed(
|
MessageUtil.sendEmbed(
|
||||||
Globals.api.owner.get(),
|
Globals.api.owner.get(),
|
||||||
EmbedBuilder()
|
EmbedBuilder()
|
||||||
|
@ -102,7 +119,7 @@ object Util {
|
||||||
.addField(
|
.addField(
|
||||||
"$e", """```
|
"$e", """```
|
||||||
${e.stackTrace.joinToString("\n")}
|
${e.stackTrace.joinToString("\n")}
|
||||||
```""".trimIndent()
|
```""".trimIndent().run { applyIf(length > 1800) { substring(1..1800) } }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ object ConfigParser {
|
||||||
|
|
||||||
fun reloadFeatures(rawConfig: RawConfig) {
|
fun reloadFeatures(rawConfig: RawConfig) {
|
||||||
Config.features = rawConfig.features?.let(::Features)
|
Config.features = rawConfig.features?.let(::Features)
|
||||||
?: Features(RawFeatures(null))
|
?: Features(RawFeatures(null, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
package moe.kageru.kagebot.config
|
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 RawWelcomeFeature(val content: List<String>?, val fallbackChannel: String?, val fallbackMessage: String?)
|
||||||
|
class RawTimeoutFeature(val role: String?)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,23 +7,26 @@ class Features(
|
||||||
debug: DebugFeature,
|
debug: DebugFeature,
|
||||||
help: HelpFeature,
|
help: HelpFeature,
|
||||||
getConfig: GetConfigFeature,
|
getConfig: GetConfigFeature,
|
||||||
setConfig: SetConfigFeature
|
setConfig: SetConfigFeature,
|
||||||
|
val timeout: TimeoutFeature?
|
||||||
) {
|
) {
|
||||||
constructor(rawFeatures: RawFeatures) : this(
|
constructor(rawFeatures: RawFeatures) : this(
|
||||||
rawFeatures.welcome?.let(::WelcomeFeature),
|
rawFeatures.welcome?.let(::WelcomeFeature),
|
||||||
DebugFeature(),
|
DebugFeature(),
|
||||||
HelpFeature(),
|
HelpFeature(),
|
||||||
GetConfigFeature(),
|
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(
|
private val featureMap = mapOf(
|
||||||
"help" to help,
|
"help" to help,
|
||||||
"debug" to debug,
|
"debug" to debug,
|
||||||
"welcome" to welcome,
|
"welcome" to welcome,
|
||||||
"getConfig" to getConfig,
|
"getConfig" to getConfig,
|
||||||
"setConfig" to setConfig
|
"setConfig" to setConfig,
|
||||||
|
"timeout" to timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
fun findByString(feature: String) = featureMap[feature]
|
fun findByString(feature: String) = featureMap[feature]
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
val embed: EmbedBuilder? by lazy {
|
||||||
rawWelcome.content?.let(MessageUtil::listToEmbed)
|
rawWelcome.content?.let(MessageUtil::listToEmbed)
|
||||||
}
|
}
|
||||||
val fallbackChannel: TextChannel? = rawWelcome.fallbackChannel?.let {
|
private val fallbackChannel: TextChannel? = rawWelcome.fallbackChannel?.let {
|
||||||
if (rawWelcome.fallbackMessage == null) {
|
if (rawWelcome.fallbackMessage == null) {
|
||||||
throw IllegalArgumentException("[feature.welcome.fallbackMessage] must not be null if fallbackChannel is defined")
|
throw IllegalArgumentException("[feature.welcome.fallbackMessage] must not be null if fallbackChannel is defined")
|
||||||
}
|
}
|
||||||
Util.findChannel(it)
|
Util.findChannel(it)
|
||||||
}
|
}
|
||||||
val fallbackMessage: String? = rawWelcome.fallbackMessage
|
private val fallbackMessage: String? = rawWelcome.fallbackMessage
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,17 @@ import org.mapdb.Serializer
|
||||||
|
|
||||||
object Dao {
|
object Dao {
|
||||||
private val db = DBMaker.fileDB("kagebot.db").fileMmapEnable().closeOnJvmShutdown().make()
|
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) {
|
fun saveTimeout(releaseTime: Long, roles: List<Long>) {
|
||||||
strings[key] = value
|
prisoners[releaseTime] = roles.toLongArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun get(key: String): String? {
|
fun getAllTimeouts() = prisoners.keys
|
||||||
return strings[key]
|
|
||||||
|
fun deleteTimeout(releaseTime: Long): LongArray {
|
||||||
|
val timeout = prisoners[releaseTime]!!
|
||||||
|
prisoners.remove(releaseTime)
|
||||||
|
return timeout
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -25,6 +25,9 @@ content = [
|
||||||
"5th", "asdasd"
|
"5th", "asdasd"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[feature.timeout]
|
||||||
|
role = "timeout"
|
||||||
|
|
||||||
[[command]]
|
[[command]]
|
||||||
trigger = "!ping"
|
trigger = "!ping"
|
||||||
response = "pong"
|
response = "pong"
|
||||||
|
@ -114,3 +117,7 @@ feature = "getConfig"
|
||||||
[[command]]
|
[[command]]
|
||||||
trigger = "!setConfig"
|
trigger = "!setConfig"
|
||||||
feature = "setConfig"
|
feature = "setConfig"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!prison"
|
||||||
|
feature = "timeout"
|
Loading…
Reference in New Issue
Block a user