Compare commits
105 Commits
Author | SHA1 | Date | |
---|---|---|---|
kageru | e3e0c07fa2 | ||
kageru | 80111b2bbf | ||
kageru | 997284fb54 | ||
kageru | 540095f677 | ||
kageru | a49fec2fcf | ||
kageru | 7c08bab460 | ||
kageru | 2b40e0578f | ||
kageru | 3838cde65e | ||
kageru | 6b21625f19 | ||
kageru | 6b52db8324 | ||
kageru | 75588b357c | ||
kageru | d7594c29e5 | ||
kageru | 91259094ed | ||
kageru | 5679d5d8dd | ||
kageru | 83a7b2cbdf | ||
kageru | a5bc947d27 | ||
kageru | 4185ee50fa | ||
kageru | d2027a2cdc | ||
kageru | d354821881 | ||
kageru | 39083d8248 | ||
kageru | 5c7efcd10e | ||
kageru | cd9ee0e881 | ||
kageru | f742383f38 | ||
kageru | 07c45d84d0 | ||
kageru | a05f2e558b | ||
kageru | f1b727662a | ||
kageru | ccf76e7c64 | ||
kageru | 69c3ae80b8 | ||
kageru | 2c56e1959a | ||
kageru | 50b97fdec7 | ||
kageru | 3d813384e2 | ||
kageru | 7eadb2d67d | ||
kageru | 288be7e4f5 | ||
kageru | a8ed168122 | ||
kageru | 5cc34c9e29 | ||
kageru | 231b27176a | ||
kageru | 1bc26951f7 | ||
kageru | 58307c9743 | ||
kageru | 2be20880b8 | ||
kageru | af65dcc06b | ||
kageru | 5a95138861 | ||
kageru | 56475ef685 | ||
kageru | d6492bae8f | ||
kageru | bb03474bf5 | ||
kageru | 17c7120796 | ||
kageru | e31d46ceb5 | ||
kageru | 87cb943712 | ||
kageru | 897457336d | ||
kageru | 1672b4c62a | ||
kageru | 828845f230 | ||
kageru | ba913f2bed | ||
kageru | e7f47b7420 | ||
kageru | c49a122622 | ||
kageru | 684926f1c5 | ||
kageru | f314c20dd8 | ||
kageru | f871d2a381 | ||
kageru | e3219c7800 | ||
kageru | 0ae5e83641 | ||
kageru | 60797c21d0 | ||
kageru | 35bd570c93 | ||
kageru | b4c2275670 | ||
kageru | 8560c3d82e | ||
kageru | c299406d69 | ||
kageru | 7bbcaba49c | ||
kageru | 3944b07ec0 | ||
kageru | 93a6e92191 | ||
kageru | 74c2f643f9 | ||
kageru | 7efeb9bace | ||
kageru | 39ceaffa6c | ||
kageru | b9f1c6e540 | ||
kageru | d425687fb5 | ||
kageru | e6f73bb178 | ||
kageru | 31c5cd7135 | ||
kageru | d6320bb5de | ||
kageru | c400ab7369 | ||
kageru | 691bc408c0 | ||
kageru | 8be98f161f | ||
kageru | cdc053a443 | ||
kageru | c8123cca4e | ||
kageru | 6069795caa | ||
kageru | f82ed7be56 | ||
kageru | d0b3bb608b | ||
kageru | 8a1308f98d | ||
kageru | b0d3475469 | ||
kageru | 911707a680 | ||
kageru | b47dc34ff0 | ||
kageru | f56df24c4f | ||
kageru | c59c0fdcd6 | ||
kageru | 23afb34a97 | ||
kageru | dc14e3ee1e | ||
kageru | 65a4ac5bed | ||
kageru | e3ab2d512f | ||
kageru | de0145a6c6 | ||
kageru | 5004de35e1 | ||
kageru | cb753b47ed | ||
kageru | 344148cd03 | ||
kageru | 731867f59e | ||
kageru | 79f84ff4dd | ||
kageru | 931a8cdf19 | ||
kageru | 32eca9e7aa | ||
kageru | 4150187bbe | ||
kageru | ac75239197 | ||
kageru | b08deb6e8d | ||
kageru | 85206a7fc3 | ||
kageru | 321999e6b2 |
|
@ -0,0 +1,8 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
|
@ -1,3 +1,8 @@
|
||||||
build/
|
build/
|
||||||
.gradle/
|
.gradle/
|
||||||
gradle/
|
gradle/
|
||||||
|
.kotlintest/
|
||||||
|
kagebot.log*
|
||||||
|
out/
|
||||||
|
secret
|
||||||
|
.idea/
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
## 1.1
|
||||||
|
- getConfig and setConfig to reload the config file at runtime via discord commands
|
||||||
|
- fixed wrong timestamps in some embeds (ty CommanderLook)
|
||||||
|
|
||||||
|
# 1.0
|
||||||
|
- feature parity with [the old bot](https://git.kageru.moe/kageru/discord-selphybot) (this does not include features that were broken in the old bot)
|
||||||
|
- proper config validation at startup time
|
|
@ -0,0 +1,21 @@
|
||||||
|
# kagebot
|
||||||
|
Kinda dead.
|
||||||
|
|
||||||
|
This bot is a replacement for [my old one](https://git.kageru.moe/kageru/discord-selphybot) with a very simple premise:
|
||||||
|
As much as possible should be configurable in a human-readable way.
|
||||||
|
This will allow anyone to modify the config to host their own instance tailored to their own needs,
|
||||||
|
and it allows server moderators to make changes without any coding experience. Even at runtime.
|
||||||
|
I try to maintain a comprehensive default configuration as part of the repository
|
||||||
|
because the past has taught me that I’m not good at updating readmes.
|
||||||
|
|
||||||
|
## A few months after
|
||||||
|
The bot has become somewhat specialized at this point,
|
||||||
|
but I think it should still be generally reusable if a similar use case arises.
|
||||||
|
The implementation has kind of deteriorated into a playground for me
|
||||||
|
(adding arrow-kt and just generally trying out FP stuff)[1],
|
||||||
|
but it’s been running and moderating a 1000+ user server for over a year
|
||||||
|
with relatively little maintenance.
|
||||||
|
|
||||||
|
[1]: While arrow is great, adding it to a project after the fact leads to a very weird combination of FP and non-FP constructs.
|
||||||
|
Would not recommend in production. This was also built in an early version of arrow that still had `Kind` and other concepts that were scrapped later,
|
||||||
|
but I don’t plan to update that ever. The bot can keep running as-is until it breaks.
|
|
@ -1,41 +1,64 @@
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
apply {
|
||||||
kotlin("jvm") version "1.3.31"
|
plugin("kotlin-kapt")
|
||||||
application
|
|
||||||
}
|
}
|
||||||
sourceSets {
|
plugins {
|
||||||
getByName("main").resources.srcDirs("src/main/resources")
|
kotlin("jvm") version "1.9.0"
|
||||||
|
id("com.github.johnrengelman.shadow") version "8.1.1" apply true
|
||||||
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets["main"].resources.srcDir("src/main/resources")
|
val botMainClass = "moe.kageru.kagebot.KagebotKt"
|
||||||
application {
|
application {
|
||||||
mainClassName = "moe.kageru.kagebot.KagebotKt"
|
mainClass.set(botMainClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Jar> {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
mapOf(
|
||||||
|
"Main-Class" to botMainClass,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "moe.kageru.kagebot"
|
group = "moe.kageru.kagebot"
|
||||||
version = "0.1"
|
version = "0.1"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url = uri("https://dl.bintray.com/arrow-kt/arrow-kt/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val test by tasks.getting(Test::class) {
|
val test by tasks.getting(Test::class) {
|
||||||
useJUnitPlatform { }
|
useJUnitPlatform { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val arrowVersion = "0.11.0"
|
||||||
dependencies {
|
dependencies {
|
||||||
compile("com.moandjiezana.toml:toml4j:0.7.2")
|
implementation("com.uchuhimo:konf-core:0.23.0")
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation("com.uchuhimo:konf-toml:0.23.0")
|
||||||
implementation("org.javacord:javacord:3.0.4")
|
implementation("org.javacord:javacord:3.8.0")
|
||||||
|
implementation("org.mapdb:mapdb:3.0.8")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.0")
|
||||||
|
implementation("com.fasterxml.jackson.core:jackson-annotations:2.11.3")
|
||||||
|
|
||||||
testImplementation("io.kotlintest:kotlintest-runner-junit5:3.3.2")
|
implementation("io.arrow-kt:arrow-core:$arrowVersion")
|
||||||
testImplementation("io.mockk:mockk:1.9")
|
implementation("io.arrow-kt:arrow-syntax:$arrowVersion")
|
||||||
// these two are needed to access javacord internals (such as reading from sent embeds during tests)
|
|
||||||
testImplementation("org.javacord:javacord-core:3.0.4")
|
testImplementation("io.kotlintest:kotlintest-runner-junit5:3.4.2")
|
||||||
testCompile("com.fasterxml.jackson.core:jackson-databind:2.9.9")
|
testImplementation("io.mockk:mockk:1.10.0")
|
||||||
|
// these two are needed to access javacord internals (such as reading from sent embeds during tests)
|
||||||
|
testImplementation("org.javacord:javacord-core:3.8.0")
|
||||||
|
testImplementation("com.fasterxml.jackson.core:jackson-databind:2.11.3")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
kotlinOptions.jvmTarget = "1.8"
|
kotlinOptions.jvmTarget = "20"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
ktlint --disabled_rules import-ordering,no-wildcard-imports && gradle test
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
if ./check.sh; then
|
||||||
|
ssh lain sudo systemctl restart selphybot.service
|
||||||
|
fi
|
|
@ -1,13 +1,10 @@
|
||||||
package moe.kageru.kagebot
|
package moe.kageru.kagebot
|
||||||
|
|
||||||
import moe.kageru.kagebot.config.Config
|
import moe.kageru.kagebot.persistence.Dao
|
||||||
import org.javacord.api.DiscordApi
|
import org.javacord.api.DiscordApi
|
||||||
import org.javacord.api.entity.server.Server
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
object Globals {
|
object Globals {
|
||||||
lateinit var server: Server
|
lateinit var api: DiscordApi
|
||||||
lateinit var api: DiscordApi
|
val commandCounter: AtomicInteger = AtomicInteger(Dao.getCommandCounter())
|
||||||
lateinit var config: Config
|
}
|
||||||
var commandCounter: AtomicInteger = AtomicInteger(0)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,81 +1,58 @@
|
||||||
package moe.kageru.kagebot
|
package moe.kageru.kagebot
|
||||||
|
|
||||||
import moe.kageru.kagebot.Log.log
|
import arrow.core.extensions.list.foldable.find
|
||||||
import moe.kageru.kagebot.Util.checked
|
import moe.kageru.kagebot.Util.checked
|
||||||
import moe.kageru.kagebot.config.Config
|
import moe.kageru.kagebot.config.Config
|
||||||
import moe.kageru.kagebot.config.RawConfig
|
import moe.kageru.kagebot.config.ConfigParser
|
||||||
|
import moe.kageru.kagebot.cron.CronD
|
||||||
|
import moe.kageru.kagebot.persistence.Dao
|
||||||
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 org.javacord.api.event.server.member.ServerMemberJoinEvent
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
Kagebot.init()
|
Kagebot.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
object Kagebot {
|
object Kagebot {
|
||||||
fun processMessage(event: MessageCreateEvent) {
|
fun MessageCreateEvent.process() {
|
||||||
if (event.messageAuthor.isBotUser) {
|
if (messageAuthor.isBotUser) {
|
||||||
if (event.messageAuthor.isYourself) {
|
handleOwn()
|
||||||
log.info("<Self> ${event.readableMessageContent}")
|
return
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for (command in Globals.config.commands) {
|
|
||||||
if (command.matches(event.messageContent)) {
|
|
||||||
command.execute(event)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Config.commands
|
||||||
|
.find { it.matches(readableMessageContent) && it.isAllowed(this) }
|
||||||
|
.map { it.execute(this) }
|
||||||
|
}
|
||||||
|
|
||||||
fun welcomeUser(event: ServerMemberJoinEvent) {
|
private fun MessageCreateEvent.handleOwn() {
|
||||||
Globals.config.features.welcome!!.let { welcome ->
|
if (messageAuthor.isYourself) {
|
||||||
val message = event.user.sendMessage(welcome.embed)
|
val loggedMessage = readableMessageContent.ifBlank { "[embed]" }
|
||||||
// If the user disabled direct messages, try the fallback (if defined)
|
Log.info("<Self> $loggedMessage")
|
||||||
if (!Util.wasSuccessful(message) &&
|
|
||||||
welcome.fallbackChannel != null &&
|
|
||||||
welcome.fallbackMessage != null
|
|
||||||
) {
|
|
||||||
welcome.fallbackChannel.sendMessage(
|
|
||||||
welcome.fallbackMessage.replace(
|
|
||||||
"@@",
|
|
||||||
MessageUtil.mention(event.user)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getSecret() = File("secret").readText().replace("\n", "")
|
fun init() {
|
||||||
|
val secret = File("secret").readText().trim()
|
||||||
fun init() {
|
val api = DiscordApiBuilder().setToken(secret).setAllIntents().login().join()
|
||||||
Globals.api = DiscordApiBuilder().setToken(getSecret()).login().join()
|
Globals.api = api
|
||||||
try {
|
ConfigParser.initialLoad(ConfigParser.DEFAULT_CONFIG_PATH).mapLeft { e ->
|
||||||
Globals.config = Config(RawConfig.read())
|
println("Config parsing error:\n$e,\n${e.message},\n${e.stackTrace.joinToString("\n")}")
|
||||||
} catch (e: IllegalArgumentException) {
|
println("Caused by: ${e.cause}\n${e.cause?.stackTrace?.joinToString("\n")}")
|
||||||
println("Config error:\n$e,\n${e.message},\n${e.stackTrace.joinToString("\n")}")
|
exitProcess(1)
|
||||||
System.exit(1)
|
|
||||||
}
|
|
||||||
Runtime.getRuntime().addShutdownHook(Thread {
|
|
||||||
log.info("Bot has been interrupted. Shutting down.")
|
|
||||||
Globals.api.disconnect()
|
|
||||||
})
|
|
||||||
log.info("kagebot Mk II running")
|
|
||||||
Globals.api.addMessageCreateListener { checked { processMessage(it) } }
|
|
||||||
Globals.config.features.welcome?.let { welcome ->
|
|
||||||
if (welcome.enabled) {
|
|
||||||
Globals.api.addServerMemberJoinListener {
|
|
||||||
checked { welcomeUser(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Globals.config.features.debug?.let { debug ->
|
|
||||||
if (debug.enabled) {
|
|
||||||
Globals.api.addMessageCreateListener {
|
|
||||||
checked { debug.handle(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Runtime.getRuntime().addShutdownHook(
|
||||||
|
Thread {
|
||||||
|
Log.info("Bot has been interrupted. Shutting down.")
|
||||||
|
Dao.setCommandCounter(Globals.commandCounter.get())
|
||||||
|
Dao.close()
|
||||||
|
api.disconnect()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Log.info("kagebot Mk II running")
|
||||||
|
api.addMessageCreateListener { checked { it.process() } }
|
||||||
|
Config.features.eventFeatures().forEach { it.register(api) }
|
||||||
|
CronD.startAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,24 +2,36 @@ package moe.kageru.kagebot
|
||||||
|
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.logging.*
|
import java.util.logging.FileHandler
|
||||||
|
import java.util.logging.Formatter
|
||||||
|
import java.util.logging.LogRecord
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
object Log {
|
object Log {
|
||||||
val log: Logger by lazy {
|
private val log: Logger by lazy {
|
||||||
val log = Logger.getGlobal()
|
Logger.getGlobal().apply {
|
||||||
val fh = FileHandler("kagebot.log", true)
|
addHandler(
|
||||||
val formatter = LogFormatter()
|
FileHandler("kagebot.log", true).apply {
|
||||||
fh.formatter = formatter
|
formatter = LogFormatter()
|
||||||
log.addHandler(fh)
|
},
|
||||||
return@lazy log
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun info(message: String) {
|
||||||
|
log.info(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun warn(message: String) {
|
||||||
|
log.warning(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LogFormatter : Formatter() {
|
private class LogFormatter : Formatter() {
|
||||||
private val timeFormatter: DateTimeFormatter =
|
private val timeFormatter: DateTimeFormatter =
|
||||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault())
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault())
|
||||||
|
|
||||||
override fun format(record: LogRecord): String {
|
override fun format(record: LogRecord): String {
|
||||||
return "[${record.level}] ${timeFormatter.format(record.instant)}: ${record.message}\n"
|
return "[${record.level}] ${timeFormatter.format(record.instant)}: ${record.message}\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,61 @@
|
||||||
package moe.kageru.kagebot
|
package moe.kageru.kagebot
|
||||||
|
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.config.SystemSpec
|
||||||
|
import org.javacord.api.entity.message.Message
|
||||||
import org.javacord.api.entity.message.MessageAuthor
|
import org.javacord.api.entity.message.MessageAuthor
|
||||||
|
import org.javacord.api.entity.message.Messageable
|
||||||
import org.javacord.api.entity.message.embed.EmbedBuilder
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
import org.javacord.api.entity.user.User
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
object MessageUtil {
|
object MessageUtil {
|
||||||
fun mention(user: MessageAuthor): String {
|
fun MessageAuthor.mention() = "<@$id>"
|
||||||
return "<@${user.id}>"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mention(user: User): String {
|
fun withEmbed(op: EmbedBuilder.() -> Unit): EmbedBuilder {
|
||||||
return "<@${user.id}>"
|
return EmbedBuilder().apply {
|
||||||
|
Config.server.icon.ifPresent { setThumbnail(it) }
|
||||||
|
setColor(SystemSpec.color)
|
||||||
|
op()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getEmbedBuilder(): EmbedBuilder {
|
fun Messageable.sendEmbed(op: EmbedBuilder.() -> Unit) {
|
||||||
val builder = EmbedBuilder()
|
val embed = withEmbed {
|
||||||
Globals.server.icon.ifPresent { builder.setThumbnail(it) }
|
setTimestampToNow()
|
||||||
return builder.setColor(Globals.config.system.color).setTimestampToNow()
|
op()
|
||||||
}
|
}
|
||||||
|
sendMessage(embed)
|
||||||
|
}
|
||||||
|
|
||||||
fun mapToEmbed(contents: Map<String, String>): EmbedBuilder {
|
/**
|
||||||
val builder = getEmbedBuilder()
|
* Send and embed and add the current time to it.
|
||||||
for ((heading, content) in contents) {
|
* The time is not set in [withEmbed] because of https://git.kageru.moe/kageru/discord-kagebot/issues/13.
|
||||||
builder.addField(heading, content)
|
*/
|
||||||
}
|
fun sendEmbed(target: Messageable, embed: EmbedBuilder): CompletableFuture<Message> {
|
||||||
return builder
|
return target.sendMessage(embed.setTimestampToNow())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason we use a list here (rather than a map) is that maps would not retain the order specified in the config.
|
||||||
|
* I tried LinkedHashMaps, but those don’t seem to work either.
|
||||||
|
*/
|
||||||
|
fun listToEmbed(contents: List<String>): EmbedBuilder {
|
||||||
|
check(contents.size % 2 != 1) { "Embed must have even number of content strings (title/content pairs)" }
|
||||||
|
return withEmbed {
|
||||||
|
contents.toPairs().forEach { (heading, content) ->
|
||||||
|
addField(heading, content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a list of elements to pairs, retaining order.
|
||||||
|
* The last element is dropped if the input size is odd.
|
||||||
|
* [1, 2, 3, 4, 5] -> [[1, 2], [3, 4]]
|
||||||
|
*/
|
||||||
|
private fun <T> Collection<T>.toPairs(): List<Pair<T, T>> = this.iterator().run {
|
||||||
|
(0 until size / 2).map {
|
||||||
|
Pair(next(), next())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,105 +1,85 @@
|
||||||
package moe.kageru.kagebot
|
package moe.kageru.kagebot
|
||||||
|
|
||||||
import moe.kageru.kagebot.Globals.api
|
import arrow.core.*
|
||||||
import moe.kageru.kagebot.Globals.server
|
import arrow.core.extensions.either.monad.flatMap
|
||||||
import moe.kageru.kagebot.Log.log
|
import arrow.core.extensions.list.foldable.find
|
||||||
|
import moe.kageru.kagebot.config.Config.server
|
||||||
|
import moe.kageru.kagebot.extensions.*
|
||||||
import org.javacord.api.entity.channel.TextChannel
|
import org.javacord.api.entity.channel.TextChannel
|
||||||
import org.javacord.api.entity.message.MessageAuthor
|
import org.javacord.api.entity.message.MessageAuthor
|
||||||
import org.javacord.api.entity.message.embed.EmbedBuilder
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
import org.javacord.api.entity.permission.Role
|
import org.javacord.api.entity.permission.Role
|
||||||
|
import org.javacord.api.entity.user.User
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.CompletionException
|
import java.util.concurrent.CompletionException
|
||||||
|
|
||||||
object Util {
|
object Util {
|
||||||
inline fun <T> T.doIf(condition: (T) -> Boolean, op: (T) -> T): T {
|
inline fun <T> T.applyIf(condition: Boolean, op: (T) -> T): T {
|
||||||
return if (condition(this)) op(this) else this
|
return if (condition) op(this) else this
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun hasOneOf(messageAuthor: MessageAuthor, roles: Set<String>): Boolean {
|
||||||
* Mimics the behavior of [Optional.ifPresent], but returns null if the optional is empty,
|
return messageAuthor.asUser().asOption().flatMap { user ->
|
||||||
* allowing easier fallback behavior via Kotlin’s ?: operator.
|
user.roles().find { it.name in roles }
|
||||||
*/
|
}.nonEmpty()
|
||||||
inline fun <T, R> Optional<T>.ifNotEmpty(op: (T) -> R): R? {
|
}
|
||||||
if (this.isPresent) {
|
|
||||||
return op(this.get())
|
private val channelIdRegex = Regex("\\d{18}")
|
||||||
|
private fun String.isEntityId() = channelIdRegex.matches(this)
|
||||||
|
|
||||||
|
fun findRole(idOrName: String): Either<String, Role> {
|
||||||
|
return when {
|
||||||
|
idOrName.isEntityId() -> server.getRoleById(idOrName).asOption().toEither { 0 }
|
||||||
|
else -> server.rolesByName(idOrName).getOnly()
|
||||||
|
}.mapLeft { "Found $it results, expected 1" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> ListK<T>.getOnly(): Either<Int, T> {
|
||||||
|
return when (size) {
|
||||||
|
1 -> Either.right(first())
|
||||||
|
else -> Either.left(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findUser(idOrName: String): Option<User> {
|
||||||
|
return when {
|
||||||
|
idOrName.isEntityId() -> server.getMemberById(idOrName).asOption()
|
||||||
|
idOrName.contains('#') -> server.getMemberByDiscriminatedNameIgnoreCase(idOrName).asOption()
|
||||||
|
else -> server.membersByName(idOrName).firstOrNone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> CompletableFuture<T>.asOption(): Option<T> {
|
||||||
|
return try {
|
||||||
|
val future = join()
|
||||||
|
if (isCompletedExceptionally) None else Option.just(future)
|
||||||
|
} catch (e: CompletionException) {
|
||||||
|
Option.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Optional<T>.asOption(): Option<T> = if (this.isPresent) Option.just(this.get()) else Option.empty()
|
||||||
|
|
||||||
|
fun findChannel(idOrName: String): Either<String, TextChannel> {
|
||||||
|
return when {
|
||||||
|
idOrName.isEntityId() -> server.channelById(idOrName).toEither { "Channel $idOrName not found" }
|
||||||
|
idOrName.startsWith('@') -> Globals.api.getUserById(idOrName.removePrefix("@")).asOption()
|
||||||
|
.toEither { "User $idOrName not found" }
|
||||||
|
.flatMap { user ->
|
||||||
|
user.openPrivateChannel().asOption().toEither { "Can’t DM user $idOrName" }
|
||||||
}
|
}
|
||||||
return null
|
else -> server.channelsByName(idOrName).getOnly().mapLeft { "Found $it channels for $idOrName, expected 1" }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun hasOneOf(messageAuthor: MessageAuthor, roles: Set<Role>): Boolean {
|
inline fun checked(op: (() -> Unit)) {
|
||||||
return messageAuthor.asUser().ifNotEmpty { user ->
|
try {
|
||||||
user.getRoles(server).toSet().intersect(roles).isNotEmpty()
|
op()
|
||||||
} ?: false
|
} catch (e: Exception) {
|
||||||
|
Log.warn("An uncaught exception occurred.\n$e")
|
||||||
|
Log.warn(e.stackTrace.joinToString("\n"))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private val channelIdRegex = Regex("\\d{18}")
|
}
|
||||||
private fun String.isEntityId() = channelIdRegex.matches(this)
|
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
|
||||||
fun findRole(idOrName: String): Role {
|
|
||||||
return when {
|
|
||||||
idOrName.isEntityId() -> server.getRoleById(idOrName).ifNotEmpty { it }
|
|
||||||
?: throw IllegalArgumentException("Role $idOrName not found.")
|
|
||||||
else -> server.getRolesByNameIgnoreCase(idOrName).let {
|
|
||||||
when (it.size) {
|
|
||||||
0 -> throw IllegalArgumentException("Role $idOrName not found.")
|
|
||||||
1 -> it[0]
|
|
||||||
else -> throw IllegalArgumentException("More than one role found with name $idOrName. Please specify the role ID instead")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> wasSuccessful(future: CompletableFuture<T>): Boolean {
|
|
||||||
try {
|
|
||||||
future.join()
|
|
||||||
} catch (e: CompletionException) {
|
|
||||||
// we don’t care about this error, but I don’t want to spam stdout
|
|
||||||
}
|
|
||||||
return !future.isCompletedExceptionally
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
|
||||||
fun findChannel(idOrName: String): TextChannel {
|
|
||||||
return when {
|
|
||||||
idOrName.isEntityId() -> server.getTextChannelById(idOrName).ifNotEmpty { it }
|
|
||||||
?: throw IllegalArgumentException("Channel ID $idOrName not found.")
|
|
||||||
else -> if (idOrName.startsWith('@')) {
|
|
||||||
api.getCachedUserByDiscriminatedName(idOrName.removePrefix("@")).ifNotEmpty { user ->
|
|
||||||
user.privateChannel.ifNotEmpty { it }
|
|
||||||
?: throw IllegalArgumentException("Could not open private channel with user $idOrName for redirection.")
|
|
||||||
}
|
|
||||||
?: throw IllegalArgumentException("Can’t find user $idOrName for redirection.")
|
|
||||||
} else {
|
|
||||||
server.getTextChannelsByName(idOrName).let {
|
|
||||||
when (it.size) {
|
|
||||||
0 -> throw IllegalArgumentException("Channel $idOrName not found.")
|
|
||||||
1 -> it[0]
|
|
||||||
else -> throw IllegalArgumentException("More than one channel found with name $idOrName. Please specify the channel ID instead")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun checked(op: (() -> Unit)) {
|
|
||||||
try {
|
|
||||||
op()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.warning("An uncaught exception occurred.\n$e")
|
|
||||||
Globals.api.owner.get().sendMessage(
|
|
||||||
EmbedBuilder()
|
|
||||||
.setTimestampToNow()
|
|
||||||
.setColor(Color.RED)
|
|
||||||
.addField("Error", "kagebot has encountered an error")
|
|
||||||
.addField(
|
|
||||||
"$e", """```
|
|
||||||
${e.stackTrace.joinToString("\n")}
|
|
||||||
```""".trimIndent()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,69 +1,60 @@
|
||||||
package moe.kageru.kagebot.command
|
package moe.kageru.kagebot.command
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import moe.kageru.kagebot.Globals
|
import moe.kageru.kagebot.Globals
|
||||||
import moe.kageru.kagebot.Globals.config
|
import moe.kageru.kagebot.Log
|
||||||
import moe.kageru.kagebot.Log.log
|
|
||||||
import moe.kageru.kagebot.MessageUtil
|
import moe.kageru.kagebot.MessageUtil
|
||||||
import moe.kageru.kagebot.Util.doIf
|
import moe.kageru.kagebot.MessageUtil.mention
|
||||||
import moe.kageru.kagebot.config.RawCommand
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.features.MessageFeature
|
||||||
import org.javacord.api.entity.message.MessageAuthor
|
import org.javacord.api.entity.message.MessageAuthor
|
||||||
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
import org.javacord.api.event.message.MessageCreateEvent
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
private const val AUTHOR_PLACEHOLDER = "@@"
|
private const val AUTHOR_PLACEHOLDER = "@@"
|
||||||
|
|
||||||
class Command(cmd: RawCommand) {
|
class Command(
|
||||||
val trigger: String
|
val trigger: String,
|
||||||
private val response: String?
|
private val response: String? = null,
|
||||||
val matchType: MatchType
|
private val permissions: Permissions?,
|
||||||
private val permissions: Permissions?
|
@JsonProperty("action")
|
||||||
private val actions: MessageActions?
|
private val actions: MessageActions?,
|
||||||
val regex: Regex?
|
embed: List<String>?,
|
||||||
|
feature: String?,
|
||||||
|
matchType: String?,
|
||||||
|
) {
|
||||||
|
val matchType: MatchType = matchType?.let { type ->
|
||||||
|
MatchType.values().find { it.name.equals(type, ignoreCase = true) }
|
||||||
|
?: throw IllegalArgumentException("Invalid [command.matchType]: “$matchType”")
|
||||||
|
} ?: MatchType.PREFIX
|
||||||
|
val regex: Regex? = if (this.matchType == MatchType.REGEX) Regex(trigger) else null
|
||||||
|
val embed: EmbedBuilder? = embed?.let(MessageUtil::listToEmbed)
|
||||||
|
private val feature: MessageFeature? = feature?.let { Config.features.findByString(it) }
|
||||||
|
|
||||||
init {
|
fun matches(msg: String) = this.matchType.matches(msg, this)
|
||||||
trigger = cmd.trigger ?: throw IllegalArgumentException("Every command must have a trigger.")
|
|
||||||
response = cmd.response
|
fun isAllowed(message: MessageCreateEvent) = permissions?.isAllowed(message) ?: true
|
||||||
matchType = cmd.matchType?.let { type ->
|
|
||||||
MatchType.values().find { it.name.equals(type, ignoreCase = true) }
|
fun execute(message: MessageCreateEvent) {
|
||||||
?: throw IllegalArgumentException("Invalid [command.matchType]: “${cmd.matchType}”")
|
Log.info("Executing command ${this.trigger} triggered by user ${message.messageAuthor.discriminatedName} (ID: ${message.messageAuthor.id})")
|
||||||
} ?: MatchType.PREFIX
|
Globals.commandCounter.incrementAndGet()
|
||||||
permissions = cmd.permissions?.let { Permissions(it) }
|
this.actions?.run(message, this)
|
||||||
actions = cmd.actions?.let { MessageActions(it) }
|
this.response?.let {
|
||||||
regex = if (matchType == MatchType.REGEX) Regex(trigger) else null
|
message.channel.sendMessage(respond(message.messageAuthor, it))
|
||||||
}
|
}
|
||||||
|
this.embed?.let {
|
||||||
fun execute(message: MessageCreateEvent) {
|
MessageUtil.sendEmbed(message.channel, embed)
|
||||||
if (permissions?.isAllowed(message) == false) {
|
|
||||||
if (config.localization.permissionDenied.isNotBlank()) {
|
|
||||||
message.channel.sendMessage(config.localization.permissionDenied)
|
|
||||||
}
|
|
||||||
log.info("Denying command ${this.trigger} to user ${message.messageAuthor.discriminatedName} (ID: ${message.messageAuthor.id})")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.info("Executing command ${this.trigger} triggered by user ${message.messageAuthor.discriminatedName} (ID: ${message.messageAuthor.id})")
|
|
||||||
Globals.commandCounter.incrementAndGet()
|
|
||||||
this.actions?.run(message, this)
|
|
||||||
this.response?.let {
|
|
||||||
message.channel.sendMessage(respond(message.messageAuthor, it))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.feature?.handle(message)
|
||||||
|
}
|
||||||
|
|
||||||
fun matches(msg: String) = this.matchType.matches(msg, this)
|
private fun respond(author: MessageAuthor, response: String) =
|
||||||
|
response.replace(AUTHOR_PLACEHOLDER, author.mention())
|
||||||
private fun respond(author: MessageAuthor, response: String) = response.doIf({ it.contains(AUTHOR_PLACEHOLDER) }) {
|
|
||||||
it.replace(AUTHOR_PLACEHOLDER, MessageUtil.mention(author))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class MatchType {
|
@Suppress("unused")
|
||||||
PREFIX {
|
enum class MatchType(val matches: (String, Command) -> Boolean) {
|
||||||
override fun matches(message: String, command: Command) = message.startsWith(command.trigger)
|
PREFIX({ message, command -> message.startsWith(command.trigger, ignoreCase = true) }),
|
||||||
},
|
CONTAINS({ message, command -> message.contains(command.trigger, ignoreCase = true) }),
|
||||||
CONTAINS {
|
REGEX({ message, command -> command.regex!!.matches(message) }),
|
||||||
override fun matches(message: String, command: Command) = message.contains(command.trigger)
|
|
||||||
},
|
|
||||||
REGEX {
|
|
||||||
override fun matches(message: String, command: Command) = command.regex!!.matches(message)
|
|
||||||
};
|
|
||||||
|
|
||||||
abstract fun matches(message: String, command: Command): Boolean
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,69 +1,38 @@
|
||||||
package moe.kageru.kagebot.command
|
package moe.kageru.kagebot.command
|
||||||
|
|
||||||
import moe.kageru.kagebot.Globals.config
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import moe.kageru.kagebot.Log.log
|
import moe.kageru.kagebot.Log
|
||||||
import moe.kageru.kagebot.MessageUtil
|
import moe.kageru.kagebot.MessageUtil.sendEmbed
|
||||||
import moe.kageru.kagebot.Util
|
import moe.kageru.kagebot.config.Config
|
||||||
import moe.kageru.kagebot.config.RawMessageActions
|
import moe.kageru.kagebot.config.LocalizationSpec
|
||||||
import moe.kageru.kagebot.config.RawRedirect
|
|
||||||
import org.javacord.api.entity.channel.TextChannel
|
|
||||||
import org.javacord.api.event.message.MessageCreateEvent
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
class MessageActions(rawActions: RawMessageActions) {
|
class MessageActions(
|
||||||
private val delete: Boolean = rawActions.delete
|
private val delete: Boolean = false,
|
||||||
private val redirect: Redirect? = rawActions.redirect?.let { Redirect(it) }
|
private val redirect: MessageRedirect?,
|
||||||
|
@JsonProperty("assign")
|
||||||
|
private val assignment: RoleAssignment?,
|
||||||
|
) {
|
||||||
|
|
||||||
fun run(message: MessageCreateEvent, command: Command) {
|
fun run(message: MessageCreateEvent, command: Command) {
|
||||||
if (delete) {
|
if (delete) {
|
||||||
deleteMessage(message)
|
deleteMessage(message)
|
||||||
}
|
|
||||||
redirect?.execute(message, command)
|
|
||||||
}
|
}
|
||||||
|
redirect?.execute(message, command)
|
||||||
|
assignment?.assign(message)
|
||||||
|
}
|
||||||
|
|
||||||
private fun deleteMessage(message: MessageCreateEvent) {
|
private fun deleteMessage(message: MessageCreateEvent) {
|
||||||
if (message.message.canYouDelete()) {
|
if (message.message.canYouDelete()) {
|
||||||
message.deleteMessage()
|
message.deleteMessage()
|
||||||
message.messageAuthor.asUser().ifPresent { user ->
|
message.messageAuthor.asUser().ifPresent { user ->
|
||||||
user.sendMessage(
|
user.sendEmbed {
|
||||||
MessageUtil.getEmbedBuilder()
|
addField("__Blacklisted__", Config.localization[LocalizationSpec.messageDeleted])
|
||||||
.addField("Blacklisted", config.localization.messageDeleted)
|
addField("Original:", "“${message.readableMessageContent}”")
|
||||||
.addField("Original:", "“${message.readableMessageContent}”")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.info("Tried to delete a message without the necessary permissions. Channel: ${message.channel.id}")
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.info("Tried to delete a message without the necessary permissions. Channel: ${message.channel.id}")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Redirect(rawRedirect: RawRedirect) {
|
|
||||||
private val target: TextChannel = rawRedirect.target?.let(Util::findChannel)
|
|
||||||
?: throw IllegalArgumentException("Every redirect needs to have a target.")
|
|
||||||
private val anonymous: Boolean = rawRedirect.anonymous
|
|
||||||
|
|
||||||
fun execute(message: MessageCreateEvent, command: Command) {
|
|
||||||
val embed = MessageUtil.getEmbedBuilder()
|
|
||||||
.addField(
|
|
||||||
config.localization.redirectedMessage,
|
|
||||||
message.readableMessageContent.let { content ->
|
|
||||||
when (command.matchType) {
|
|
||||||
MatchType.PREFIX -> content.removePrefix(command.trigger).trim()
|
|
||||||
else -> content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// No inlined if/else because the types are different.
|
|
||||||
// Passing the full message author will also include the avatar in the embed.
|
|
||||||
embed.apply {
|
|
||||||
if (anonymous) {
|
|
||||||
setAuthor("Anonymous")
|
|
||||||
} else {
|
|
||||||
setAuthor(message.messageAuthor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Util.wasSuccessful(target.sendMessage(embed))) {
|
|
||||||
log.warning("Could not redirect message to channel $target")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package moe.kageru.kagebot.command
|
||||||
|
|
||||||
|
import moe.kageru.kagebot.Log
|
||||||
|
import moe.kageru.kagebot.MessageUtil
|
||||||
|
import moe.kageru.kagebot.Util
|
||||||
|
import moe.kageru.kagebot.Util.applyIf
|
||||||
|
import moe.kageru.kagebot.Util.asOption
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.config.LocalizationSpec
|
||||||
|
import moe.kageru.kagebot.extensions.unwrap
|
||||||
|
import org.javacord.api.entity.channel.TextChannel
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
|
class MessageRedirect(target: String, private val anonymous: Boolean = false) {
|
||||||
|
private val targetChannel: TextChannel = Util.findChannel(target).unwrap()
|
||||||
|
|
||||||
|
fun execute(message: MessageCreateEvent, command: Command) {
|
||||||
|
val embed = MessageUtil.withEmbed {
|
||||||
|
val redirectedText = message.readableMessageContent
|
||||||
|
.applyIf(command.matchType == MatchType.PREFIX) { content ->
|
||||||
|
content.removePrefix(command.trigger).trim()
|
||||||
|
}
|
||||||
|
addField(Config.localization[LocalizationSpec.redirectedMessage], redirectedText)
|
||||||
|
Log.info("Redirected message: $redirectedText")
|
||||||
|
}
|
||||||
|
// No inlined if/else because the types are different.
|
||||||
|
// Passing the full message author will also include the avatar in the embed.
|
||||||
|
embed.apply {
|
||||||
|
if (anonymous) {
|
||||||
|
setAuthor("Anonymous")
|
||||||
|
} else {
|
||||||
|
setAuthor(message.messageAuthor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MessageUtil.sendEmbed(targetChannel, embed).asOption().isEmpty()) {
|
||||||
|
Log.warn("Could not redirect message to channel $targetChannel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,34 +1,23 @@
|
||||||
package moe.kageru.kagebot.command
|
package moe.kageru.kagebot.command
|
||||||
|
|
||||||
|
import arrow.core.Option
|
||||||
|
import arrow.core.toOption
|
||||||
import moe.kageru.kagebot.Util
|
import moe.kageru.kagebot.Util
|
||||||
import moe.kageru.kagebot.config.RawPermissions
|
|
||||||
import org.javacord.api.entity.permission.Role
|
|
||||||
import org.javacord.api.event.message.MessageCreateEvent
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
class Permissions(perms: RawPermissions) {
|
class Permissions(
|
||||||
private val hasOneOf: Set<Role>?
|
hasOneOf: List<String>?,
|
||||||
private val hasNoneOf: Set<Role>?
|
hasNoneOf: List<String>?,
|
||||||
private val onlyDM: Boolean
|
private val onlyDM: Boolean = false,
|
||||||
|
) {
|
||||||
|
private val hasOneOf: Option<Set<String>> = hasOneOf?.toSet().toOption()
|
||||||
|
private val hasNoneOf: Option<Set<String>> = hasNoneOf?.toSet().toOption()
|
||||||
|
|
||||||
init {
|
fun isAllowed(message: MessageCreateEvent): Boolean = when {
|
||||||
hasOneOf = perms.hasOneOf?.mapTo(mutableSetOf(), Util::findRole)
|
message.messageAuthor.isBotOwner -> true
|
||||||
hasNoneOf = perms.hasNoneOf?.mapTo(mutableSetOf(), Util::findRole)
|
onlyDM && !message.isPrivateMessage -> false
|
||||||
onlyDM = perms.onlyDM
|
// returns true if the Option is empty (case for no restrictions)
|
||||||
}
|
else -> hasOneOf.forall { Util.hasOneOf(message.messageAuthor, it) } &&
|
||||||
|
hasNoneOf.forall { !Util.hasOneOf(message.messageAuthor, it) }
|
||||||
fun isAllowed(message: MessageCreateEvent): Boolean {
|
}
|
||||||
if (message.messageAuthor.isBotOwner) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (onlyDM && !message.isPrivateMessage) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
hasOneOf?.let { roles ->
|
|
||||||
if (!Util.hasOneOf(message.messageAuthor, roles)) return false
|
|
||||||
}
|
|
||||||
hasNoneOf?.let { roles ->
|
|
||||||
if (Util.hasOneOf(message.messageAuthor, roles)) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package moe.kageru.kagebot.command
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import moe.kageru.kagebot.Log
|
||||||
|
import moe.kageru.kagebot.Util
|
||||||
|
import moe.kageru.kagebot.extensions.getUser
|
||||||
|
import moe.kageru.kagebot.extensions.unwrap
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
|
class RoleAssignment(@JsonProperty("role") role: String) {
|
||||||
|
private val role = Util.findRole(role).unwrap()
|
||||||
|
|
||||||
|
fun assign(message: MessageCreateEvent) = message.getUser().fold(
|
||||||
|
{ Log.warn("Could not find user ${message.messageAuthor.name} for role assign") },
|
||||||
|
{ it.addRole(role, "Requested via command.") },
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,53 +1,26 @@
|
||||||
package moe.kageru.kagebot.config
|
package moe.kageru.kagebot.config
|
||||||
|
|
||||||
import moe.kageru.kagebot.Globals
|
import arrow.core.ListK
|
||||||
import moe.kageru.kagebot.Globals.api
|
import arrow.core.k
|
||||||
|
import com.uchuhimo.konf.Config
|
||||||
|
import com.uchuhimo.konf.source.toml
|
||||||
import moe.kageru.kagebot.command.Command
|
import moe.kageru.kagebot.command.Command
|
||||||
import moe.kageru.kagebot.features.Features
|
import moe.kageru.kagebot.features.Features
|
||||||
import java.awt.Color
|
import org.javacord.api.entity.server.Server
|
||||||
|
|
||||||
class Config(rawConfig: RawConfig) {
|
object Config {
|
||||||
val system: SystemConfig = rawConfig.system?.let(::SystemConfig)
|
val systemSpec = Config { addSpec(SystemSpec) }.from.toml
|
||||||
?: throw IllegalArgumentException("No [system] block in config.")
|
val localeSpec = Config { addSpec(LocalizationSpec) }.from.toml
|
||||||
var localization: Localization = rawConfig.localization?.let(::Localization)
|
val commandSpec = Config { addSpec(CommandSpec) }.from.toml
|
||||||
?: throw IllegalArgumentException("No [localization] block in config.")
|
val featureSpec = Config { addSpec(FeatureSpec) }.from.toml
|
||||||
var commands: List<Command>
|
|
||||||
var features: Features
|
|
||||||
|
|
||||||
init {
|
lateinit var system: Config
|
||||||
Globals.server = api.getServerById(system.serverId).orElseThrow()
|
lateinit var localization: Config
|
||||||
this.commands = rawConfig.commands?.map(::Command) ?: emptyList()
|
lateinit var commandConfig: Config
|
||||||
this.features = rawConfig.features?.let(::Features) ?: Features.NONE
|
lateinit var featureConfig: Config
|
||||||
}
|
lateinit var server: Server
|
||||||
|
|
||||||
fun reloadLocalization(rawLocalization: RawLocalization) {
|
// for easier access
|
||||||
this.localization = Localization(rawLocalization)
|
val features: Features get() = featureConfig[FeatureSpec.features]
|
||||||
}
|
val commands: ListK<Command> get() = commandConfig[CommandSpec.command].k()
|
||||||
|
|
||||||
fun reloadCommands(rawConfig: RawConfig) {
|
|
||||||
this.commands = rawConfig.commands?.map(::Command)
|
|
||||||
?: throw IllegalArgumentException("No commands found in config.")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reloadFeatures(rawFeatures: RawFeatures) {
|
|
||||||
this.features = Features(rawFeatures)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SystemConfig(val serverId: String, val color: Color) {
|
|
||||||
constructor(rawSystemConfig: RawSystemConfig) : this(
|
|
||||||
rawSystemConfig.serverId ?: throw IllegalArgumentException("No [system.server] defined."),
|
|
||||||
Color.decode(rawSystemConfig.color ?: "#1793d0")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Localization(val permissionDenied: String, val redirectedMessage: String, val messageDeleted: String) {
|
|
||||||
constructor(rawLocalization: RawLocalization) : this(
|
|
||||||
permissionDenied = rawLocalization.permissionDenied
|
|
||||||
?: throw IllegalArgumentException("No [localization.permissionDenied] defined"),
|
|
||||||
redirectedMessage = rawLocalization.redirectedMessage
|
|
||||||
?: throw IllegalArgumentException("No [localization.redirectMessage] defined"),
|
|
||||||
messageDeleted = rawLocalization.messageDeleted
|
|
||||||
?: throw IllegalArgumentException("No [localization.messageDeleted] defined")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package moe.kageru.kagebot.config
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import moe.kageru.kagebot.Globals
|
||||||
|
import moe.kageru.kagebot.config.SystemSpec.serverId
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object ConfigParser {
|
||||||
|
internal const val DEFAULT_CONFIG_PATH = "config.toml"
|
||||||
|
val configFile: File = File(DEFAULT_CONFIG_PATH)
|
||||||
|
|
||||||
|
fun initialLoad(file: String) = runBlocking {
|
||||||
|
Either.catch {
|
||||||
|
val configFile = getFile(file)
|
||||||
|
val config = Config.systemSpec.file(configFile)
|
||||||
|
Config.system = config
|
||||||
|
Config.server = Globals.api.getServerById(config[serverId])
|
||||||
|
.orElseThrow { IllegalArgumentException("Invalid server configured.") }
|
||||||
|
Config.localization = Config.localeSpec.file(configFile)
|
||||||
|
Config.featureConfig = Config.featureSpec.file(configFile)
|
||||||
|
Config.commandConfig = Config.commandSpec.file(configFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFile(path: String): File {
|
||||||
|
val file = File(path)
|
||||||
|
if (file.isFile) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
println("Config not found, falling back to defaults...")
|
||||||
|
return File(this::class.java.classLoader.getResource(path)!!.toURI())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package moe.kageru.kagebot.config
|
||||||
|
|
||||||
|
import com.uchuhimo.konf.ConfigSpec
|
||||||
|
import moe.kageru.kagebot.command.Command
|
||||||
|
import moe.kageru.kagebot.config.Config.system
|
||||||
|
import moe.kageru.kagebot.features.Features
|
||||||
|
import java.awt.Color
|
||||||
|
|
||||||
|
object SystemSpec : ConfigSpec() {
|
||||||
|
private val rawColor by optional("#1793d0", name = "color")
|
||||||
|
val serverId by required<String>()
|
||||||
|
val color by kotlin.lazy { Color.decode(system[rawColor])!! }
|
||||||
|
}
|
||||||
|
|
||||||
|
object LocalizationSpec : ConfigSpec() {
|
||||||
|
val redirectedMessage by optional("says")
|
||||||
|
val messageDeleted by optional("Your message was deleted.")
|
||||||
|
val timeout by optional("You have been timed out for @@ minutes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
object CommandSpec : ConfigSpec(prefix = "") {
|
||||||
|
val command by optional(emptyList<Command>())
|
||||||
|
}
|
||||||
|
|
||||||
|
object FeatureSpec : ConfigSpec(prefix = "") {
|
||||||
|
val features by optional(Features(), name = "feature")
|
||||||
|
}
|
|
@ -1,52 +0,0 @@
|
||||||
package moe.kageru.kagebot.config
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import com.moandjiezana.toml.Toml
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class RawConfig(
|
|
||||||
val system: RawSystemConfig?,
|
|
||||||
val localization: RawLocalization?,
|
|
||||||
@SerializedName("command") val commands: List<RawCommand>?,
|
|
||||||
@SerializedName("feature") val features: RawFeatures?
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
const val DEFAULT_CONFIG_PATH = "config.toml"
|
|
||||||
|
|
||||||
fun readFromString(tomlContent: String) = Toml().read(tomlContent).to(RawConfig::class.java)
|
|
||||||
|
|
||||||
fun read(path: String = DEFAULT_CONFIG_PATH): RawConfig {
|
|
||||||
val toml: Toml = Toml().read(run {
|
|
||||||
val file = File(path)
|
|
||||||
if (file.isFile) {
|
|
||||||
return@run file
|
|
||||||
}
|
|
||||||
println("Config not found, falling back to defaults...")
|
|
||||||
File(this::class.java.classLoader.getResource(path)!!.toURI())
|
|
||||||
})
|
|
||||||
return toml.to(RawConfig::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RawSystemConfig(val serverId: String?, val color: String?)
|
|
||||||
class RawLocalization(val permissionDenied: String?, val redirectedMessage: String?, val messageDeleted: String?)
|
|
||||||
class RawCommand(
|
|
||||||
val trigger: String?,
|
|
||||||
val response: String?,
|
|
||||||
val matchType: String?,
|
|
||||||
val permissions: RawPermissions?,
|
|
||||||
@SerializedName("action") val actions: RawMessageActions?
|
|
||||||
)
|
|
||||||
|
|
||||||
class RawPermissions(val hasOneOf: List<String>?, val hasNoneOf: List<String>?, val onlyDM: Boolean)
|
|
||||||
class RawMessageActions(val delete: Boolean, val redirect: RawRedirect?)
|
|
||||||
class RawRedirect(val target: String?, val anonymous: Boolean)
|
|
||||||
class RawFeatures(val welcome: RawWelcomeFeature?, val debug: RawDebugFeatures?)
|
|
||||||
class RawWelcomeFeature(
|
|
||||||
val enabled: Boolean,
|
|
||||||
val content: Map<String, String>?,
|
|
||||||
val fallbackChannel: String?,
|
|
||||||
val fallbackMessage: String?
|
|
||||||
)
|
|
||||||
class RawDebugFeatures(var enabled: Boolean)
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package moe.kageru.kagebot.extensions
|
||||||
|
|
||||||
|
import arrow.Kind
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.Tuple3
|
||||||
|
import arrow.core.getOrElse
|
||||||
|
import arrow.typeclasses.Functor
|
||||||
|
import moe.kageru.kagebot.Log
|
||||||
|
|
||||||
|
fun <L, R> Either<L, R>.on(op: (R) -> Unit): Either<L, R> {
|
||||||
|
this.map { op(it) }
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Either<*, T>.unwrap(): T = getOrElse {
|
||||||
|
Log.warn("Attempted to unwrap $this")
|
||||||
|
error("Attempted to unwrap Either.left")
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <A, B, C, A2, F> Tuple3<A, B, C>.mapFirst(AP: Functor<F>, op: (A) -> Kind<F, A2>) = let { (a, b, c) ->
|
||||||
|
AP.run { op(a).map { Tuple3(it, b, c) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <A, B, C, B2, F> Tuple3<A, B, C>.mapSecond(AP: Functor<F>, op: (B) -> Kind<F, B2>) = let { (a, b, c) ->
|
||||||
|
AP.run { op(b).map { Tuple3(a, it, c) } }
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package moe.kageru.kagebot.extensions
|
||||||
|
|
||||||
|
import arrow.core.ListK
|
||||||
|
import arrow.core.Option
|
||||||
|
import arrow.core.k
|
||||||
|
import moe.kageru.kagebot.Util.asOption
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import org.javacord.api.entity.channel.ChannelCategory
|
||||||
|
import org.javacord.api.entity.channel.ServerTextChannel
|
||||||
|
import org.javacord.api.entity.permission.Role
|
||||||
|
import org.javacord.api.entity.server.Server
|
||||||
|
import org.javacord.api.entity.user.User
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
|
fun Server.channelById(id: String): Option<ServerTextChannel> = getTextChannelById(id).asOption()
|
||||||
|
fun Server.channelsByName(name: String): ListK<ServerTextChannel> = getTextChannelsByName(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.memberById(name: Long): Option<User> = getMemberById(name).asOption()
|
||||||
|
fun Server.categoriesByName(name: String): ListK<ChannelCategory> = getChannelCategoriesByNameIgnoreCase(name).k()
|
||||||
|
|
||||||
|
fun MessageCreateEvent.getUser(): Option<User> = Config.server.memberById(messageAuthor.id)
|
||||||
|
|
||||||
|
fun User.roles(): ListK<Role> = getRoles(Config.server).k()
|
|
@ -0,0 +1,65 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import com.sun.management.OperatingSystemMXBean
|
||||||
|
import moe.kageru.kagebot.Globals
|
||||||
|
import moe.kageru.kagebot.MessageUtil
|
||||||
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
import java.lang.management.ManagementFactory
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
class DebugFeature : MessageFeature {
|
||||||
|
|
||||||
|
override fun handle(message: MessageCreateEvent) {
|
||||||
|
if (message.messageAuthor.isBotOwner) {
|
||||||
|
MessageUtil.sendEmbed(message.channel, getPerformanceStats())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPerformanceStats(): EmbedBuilder {
|
||||||
|
val osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean::class.java)
|
||||||
|
val runtime = Runtime.getRuntime()
|
||||||
|
return MessageUtil.listToEmbed(
|
||||||
|
listOf(
|
||||||
|
"Bot:",
|
||||||
|
getBotStats(),
|
||||||
|
"Memory:",
|
||||||
|
getMemoryInfo(runtime, osBean),
|
||||||
|
"CPU:",
|
||||||
|
getCpuInfo(osBean),
|
||||||
|
"System:",
|
||||||
|
getOsInfo(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBotStats() = "kagebot has been running for ${getBotUptime()}.\n" +
|
||||||
|
"During this time, ${Globals.commandCounter.get()} commands have been executed."
|
||||||
|
|
||||||
|
private fun getBotUptime(): String {
|
||||||
|
val uptime = Duration.of(ManagementFactory.getRuntimeMXBean().uptime, ChronoUnit.MILLIS)
|
||||||
|
return String.format(
|
||||||
|
"%d days, %d hours, %d minutes, %d seconds",
|
||||||
|
uptime.toDaysPart(),
|
||||||
|
uptime.toHoursPart(),
|
||||||
|
uptime.toMinutesPart(),
|
||||||
|
uptime.toSecondsPart(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMemoryInfo(runtime: Runtime, osBean: OperatingSystemMXBean): String {
|
||||||
|
val mb = 1024 * 1024
|
||||||
|
return "Memory usage: ${(runtime.totalMemory() - runtime.freeMemory()) / mb} MB.\n" +
|
||||||
|
"Total system memory: ${osBean.committedVirtualMemorySize / mb}/" +
|
||||||
|
"${osBean.totalPhysicalMemorySize / mb} MB."
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCpuInfo(osBean: OperatingSystemMXBean) =
|
||||||
|
"The bot is currently using ${String.format("%.4f", osBean.processCpuLoad * 100)}% of the CPU with " +
|
||||||
|
"${Thread.activeCount()} active threads.\n" +
|
||||||
|
"Total system load is ${String.format("%.2f", osBean.systemCpuLoad * 100)}%."
|
||||||
|
|
||||||
|
private fun getOsInfo() = "Running on ${System.getProperty("os.name")} " +
|
||||||
|
"${System.getProperty("os.version")}-${System.getProperty("os.arch")}.\n"
|
||||||
|
}
|
|
@ -1,77 +0,0 @@
|
||||||
package moe.kageru.kagebot.features
|
|
||||||
|
|
||||||
import com.sun.management.OperatingSystemMXBean
|
|
||||||
import moe.kageru.kagebot.Globals
|
|
||||||
import moe.kageru.kagebot.MessageUtil
|
|
||||||
import moe.kageru.kagebot.config.RawDebugFeatures
|
|
||||||
import org.javacord.api.entity.message.embed.EmbedBuilder
|
|
||||||
import org.javacord.api.event.message.MessageCreateEvent
|
|
||||||
import java.lang.management.ManagementFactory
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.temporal.ChronoUnit
|
|
||||||
|
|
||||||
class DebugFeatures(rawDebugFeatures: RawDebugFeatures) {
|
|
||||||
val enabled: Boolean = rawDebugFeatures.enabled
|
|
||||||
|
|
||||||
fun handle(message: MessageCreateEvent) {
|
|
||||||
if (message.messageAuthor.isBotOwner) {
|
|
||||||
if (message.readableMessageContent.startsWith("!debugstats")) {
|
|
||||||
message.channel.sendMessage(getPerformanceStats())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPerformanceStats(): EmbedBuilder {
|
|
||||||
val osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean::class.java)
|
|
||||||
val runtime = Runtime.getRuntime()
|
|
||||||
return MessageUtil.mapToEmbed(
|
|
||||||
mapOf(
|
|
||||||
Pair(
|
|
||||||
"Bot:",
|
|
||||||
getBotStats()
|
|
||||||
),
|
|
||||||
Pair(
|
|
||||||
"Memory:",
|
|
||||||
getMemoryInfo(runtime, osBean)
|
|
||||||
),
|
|
||||||
Pair(
|
|
||||||
"CPU:",
|
|
||||||
getCpuInfo(osBean)
|
|
||||||
),
|
|
||||||
Pair(
|
|
||||||
"System:",
|
|
||||||
getOsInfo()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBotStats() = "kagebot has been running for ${getBotUptime()}.\n" +
|
|
||||||
"During this time, ${Globals.commandCounter.incrementAndGet()} commands have been executed."
|
|
||||||
|
|
||||||
private fun getBotUptime(): String {
|
|
||||||
val uptime = Duration.of(ManagementFactory.getRuntimeMXBean().uptime, ChronoUnit.MILLIS)
|
|
||||||
return String.format(
|
|
||||||
"%d days, %d hours, %d minutes, %d seconds",
|
|
||||||
uptime.toDaysPart(),
|
|
||||||
uptime.toHoursPart(),
|
|
||||||
uptime.toMinutesPart(),
|
|
||||||
uptime.toSecondsPart()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMemoryInfo(runtime: Runtime, osBean: OperatingSystemMXBean): String {
|
|
||||||
val mb = 1024 * 1024
|
|
||||||
return "Memory usage: ${(runtime.totalMemory() - runtime.freeMemory()) / mb} MB.\n" +
|
|
||||||
"Total system memory: ${osBean.committedVirtualMemorySize / mb}/" +
|
|
||||||
"${osBean.totalPhysicalMemorySize / mb} MB."
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCpuInfo(osBean: OperatingSystemMXBean) =
|
|
||||||
"The bot is currently using ${String.format("%.4f", osBean.processCpuLoad * 100)}% of the CPU with " +
|
|
||||||
"${Thread.activeCount()} active threads.\n" +
|
|
||||||
"Total system load is ${String.format("%.2f", osBean.systemCpuLoad * 100)}%."
|
|
||||||
|
|
||||||
private fun getOsInfo() = "Running on ${System.getProperty("os.name")} " +
|
|
||||||
"${System.getProperty("os.version")}-${System.getProperty("os.arch")}.\n"
|
|
||||||
}
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import org.javacord.api.DiscordApi
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
|
interface MessageFeature {
|
||||||
|
fun handle(message: MessageCreateEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EventFeature {
|
||||||
|
fun register(api: DiscordApi)
|
||||||
|
}
|
|
@ -1,15 +1,26 @@
|
||||||
package moe.kageru.kagebot.features
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
import moe.kageru.kagebot.config.RawFeatures
|
class Features(
|
||||||
|
val welcome: WelcomeFeature? = null,
|
||||||
|
val timeout: TimeoutFeature? = null,
|
||||||
|
vc: TempVCFeature = TempVCFeature(null),
|
||||||
|
) {
|
||||||
|
private val debug = DebugFeature()
|
||||||
|
private val help = HelpFeature()
|
||||||
|
private val getConfig = GetConfigFeature()
|
||||||
|
private val setConfig = SetConfigFeature()
|
||||||
|
|
||||||
class Features(val welcome: WelcomeFeature?, val debug: DebugFeatures?) {
|
private val all = listOf(welcome, debug, help, getConfig, setConfig, timeout, vc)
|
||||||
|
private val featureMap = mapOf(
|
||||||
|
"help" to help,
|
||||||
|
"debug" to debug,
|
||||||
|
"welcome" to welcome,
|
||||||
|
"getConfig" to getConfig,
|
||||||
|
"setConfig" to setConfig,
|
||||||
|
"timeout" to timeout,
|
||||||
|
"vc" to vc,
|
||||||
|
)
|
||||||
|
|
||||||
constructor(rawFeatures: RawFeatures) : this(
|
fun findByString(feature: String) = featureMap[feature]
|
||||||
rawFeatures.welcome?.let(::WelcomeFeature),
|
fun eventFeatures() = all.filterIsInstance<EventFeature>()
|
||||||
rawFeatures.debug?.let(::DebugFeatures)
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val NONE = Features(null, null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import moe.kageru.kagebot.config.ConfigParser
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple message handler to send the current config file via message attachment.
|
||||||
|
*/
|
||||||
|
class GetConfigFeature : MessageFeature {
|
||||||
|
override fun handle(message: MessageCreateEvent) {
|
||||||
|
message.channel.sendMessage(ConfigParser.configFile)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import arrow.core.extensions.listk.functorFilter.filter
|
||||||
|
import moe.kageru.kagebot.MessageUtil.sendEmbed
|
||||||
|
import moe.kageru.kagebot.command.MatchType
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
|
class HelpFeature : MessageFeature {
|
||||||
|
override fun handle(message: MessageCreateEvent) {
|
||||||
|
message.channel.sendEmbed {
|
||||||
|
addField("Commands:", listCommands(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listCommands(message: MessageCreateEvent) = Config.commands
|
||||||
|
.filter { it.matchType == MatchType.PREFIX && it.isAllowed(message) }
|
||||||
|
.joinToString("\n") { it.trigger }
|
|
@ -0,0 +1,28 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import moe.kageru.kagebot.MessageUtil.sendEmbed
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.config.ConfigParser
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
|
class SetConfigFeature : MessageFeature {
|
||||||
|
@ExperimentalStdlibApi
|
||||||
|
override fun handle(message: MessageCreateEvent) {
|
||||||
|
if (message.messageAttachments.size != 1) {
|
||||||
|
message.channel.sendMessage("Error: please attach the new config to your message.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val newConfig = message.messageAttachments[0].url.openStream().readAllBytes().decodeToString()
|
||||||
|
try {
|
||||||
|
Config.localization = Config.localeSpec.string(newConfig)
|
||||||
|
Config.featureConfig = Config.featureSpec.string(newConfig)
|
||||||
|
Config.commandConfig = Config.commandSpec.string(newConfig)
|
||||||
|
ConfigParser.configFile.writeText(newConfig)
|
||||||
|
message.channel.sendMessage("Config reloaded.")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
message.channel.sendEmbed {
|
||||||
|
addField("Error", "```${e.message}```")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.filterOrElse
|
||||||
|
import arrow.core.flatMap
|
||||||
|
import arrow.core.rightIfNotNull
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import moe.kageru.kagebot.Log
|
||||||
|
import moe.kageru.kagebot.Util.asOption
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.extensions.categoriesByName
|
||||||
|
import moe.kageru.kagebot.persistence.Dao
|
||||||
|
import org.javacord.api.DiscordApi
|
||||||
|
import org.javacord.api.entity.channel.ChannelCategory
|
||||||
|
import org.javacord.api.entity.channel.ServerVoiceChannel
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
|
class TempVCFeature(@JsonProperty("category") category: String? = null) : EventFeature, MessageFeature {
|
||||||
|
private val category: ChannelCategory? = category?.let { Config.server.categoriesByName(it).first() }
|
||||||
|
|
||||||
|
override fun handle(message: MessageCreateEvent): Unit = with(message) {
|
||||||
|
Either.cond(
|
||||||
|
' ' in readableMessageContent,
|
||||||
|
{ readableMessageContent.split(' ', limit = 2).last() },
|
||||||
|
{ "Invalid syntax, expected `<command> <userlimit>`" },
|
||||||
|
)
|
||||||
|
.flatMap { limit ->
|
||||||
|
limit.toIntOrNull().rightIfNotNull { "Invalid syntax, expected a number as limit, got $limit" }
|
||||||
|
}.filterOrElse({ it < 99 }, { "Error: can’t create a channel with that many users." })
|
||||||
|
.fold(
|
||||||
|
{ err -> channel.sendMessage(err) },
|
||||||
|
{ limit ->
|
||||||
|
createChannel(message, limit)
|
||||||
|
channel.sendMessage("Done")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun register(api: DiscordApi) {
|
||||||
|
api.addServerVoiceChannelMemberLeaveListener { event ->
|
||||||
|
if (event.channel.connectedUsers.isEmpty() && Dao.isTemporaryVC(event.channel.idAsString)) {
|
||||||
|
deleteChannel(event.channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteChannel(channel: ServerVoiceChannel) =
|
||||||
|
channel.delete("Empty temporary channel").asOption().fold(
|
||||||
|
{ Log.warn("Attempted to delete temporary VC without the necessary permissions") },
|
||||||
|
{ Dao.removeTemporaryVC(channel.idAsString) },
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createChannel(message: MessageCreateEvent, limit: Int): Unit =
|
||||||
|
Config.server.createVoiceChannelBuilder().apply {
|
||||||
|
setUserlimit(limit)
|
||||||
|
setName(generateChannelName(message))
|
||||||
|
setAuditLogReason("Created temporary VC for user ${message.messageAuthor.discriminatedName}")
|
||||||
|
setCategory(category)
|
||||||
|
}.create().asOption().fold(
|
||||||
|
{ Log.warn("Attempted to create temporary VC without the necessary permissions") },
|
||||||
|
{ channel -> Dao.addTemporaryVC(channel.idAsString) },
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun generateChannelName(message: MessageCreateEvent): String =
|
||||||
|
"${message.messageAuthor.name}’s volatile corner"
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import arrow.core.*
|
||||||
|
import arrow.core.extensions.either.applicative.applicative
|
||||||
|
import arrow.core.extensions.either.monad.flatMap
|
||||||
|
import arrow.core.extensions.list.monad.map
|
||||||
|
import arrow.core.extensions.listk.functorFilter.filter
|
||||||
|
import arrow.core.extensions.option.applicative.applicative
|
||||||
|
import arrow.syntax.collections.destructured
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import moe.kageru.kagebot.Log
|
||||||
|
import moe.kageru.kagebot.MessageUtil.sendEmbed
|
||||||
|
import moe.kageru.kagebot.Util.findRole
|
||||||
|
import moe.kageru.kagebot.Util.findUser
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.config.LocalizationSpec
|
||||||
|
import moe.kageru.kagebot.extensions.*
|
||||||
|
import moe.kageru.kagebot.persistence.Dao
|
||||||
|
import org.javacord.api.entity.permission.Role
|
||||||
|
import org.javacord.api.entity.user.User
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
class TimeoutFeature(@JsonProperty("role") role: String) : MessageFeature {
|
||||||
|
private val timeoutRole: Role = findRole(role).unwrap()
|
||||||
|
|
||||||
|
override fun handle(message: MessageCreateEvent) {
|
||||||
|
message.readableMessageContent.split(' ', limit = 4).let { args ->
|
||||||
|
Either.cond(
|
||||||
|
args.size >= 3,
|
||||||
|
{ 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." },
|
||||||
|
).flatMap {
|
||||||
|
it.mapFirst(Option.applicative(), ::findUser).fix()
|
||||||
|
.toEither { "Error: User ${it.a} not found, consider using the user ID" }
|
||||||
|
}.flatMap {
|
||||||
|
it.mapSecond(Either.applicative()) { time ->
|
||||||
|
time.toLongOrNull().rightIfNotNull { "Error: malformed time “${it.b}”" }
|
||||||
|
}.fix()
|
||||||
|
}.on { (user, time, _) ->
|
||||||
|
applyTimeout(user, time)
|
||||||
|
}.fold(
|
||||||
|
{ error -> message.channel.sendMessage(error) },
|
||||||
|
{ (user, time, reason) ->
|
||||||
|
user.sendEmbed {
|
||||||
|
addField("Timeout", Config.localization[LocalizationSpec.timeout].replace("@@", "$time"))
|
||||||
|
reason?.let { addField("Reason", it) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()}")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,53 @@
|
||||||
package moe.kageru.kagebot.features
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import moe.kageru.kagebot.Log
|
||||||
import moe.kageru.kagebot.MessageUtil
|
import moe.kageru.kagebot.MessageUtil
|
||||||
import moe.kageru.kagebot.Util
|
import moe.kageru.kagebot.Util
|
||||||
import moe.kageru.kagebot.config.RawWelcomeFeature
|
import moe.kageru.kagebot.Util.asOption
|
||||||
|
import moe.kageru.kagebot.Util.checked
|
||||||
|
import moe.kageru.kagebot.extensions.unwrap
|
||||||
|
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
|
||||||
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
import org.javacord.api.event.server.member.ServerMemberJoinEvent
|
||||||
|
|
||||||
class WelcomeFeature(rawWelcome: RawWelcomeFeature) {
|
class WelcomeFeature(
|
||||||
val enabled: Boolean = rawWelcome.enabled
|
content: List<String>?,
|
||||||
val embed: EmbedBuilder? by lazy {
|
fallbackChannel: String?,
|
||||||
rawWelcome.content?.let(MessageUtil::mapToEmbed)
|
private val fallbackMessage: String?,
|
||||||
|
) : MessageFeature, EventFeature {
|
||||||
|
val embed: EmbedBuilder? by lazy { content?.let(MessageUtil::listToEmbed) }
|
||||||
|
|
||||||
|
override fun register(api: DiscordApi) {
|
||||||
|
api.addServerMemberJoinListener { event ->
|
||||||
|
checked { welcomeUser(event) }
|
||||||
}
|
}
|
||||||
val fallbackChannel: TextChannel? = rawWelcome.fallbackChannel?.let {
|
}
|
||||||
if (rawWelcome.fallbackMessage == null) {
|
|
||||||
throw IllegalArgumentException("[feature.welcome.fallbackMessage] must not be null if fallbackChannel is defined")
|
fun welcomeUser(event: ServerMemberJoinEvent) {
|
||||||
}
|
Log.info("User ${event.user.discriminatedName} joined")
|
||||||
Util.findChannel(it)
|
val message = event.user.sendMessage(embed)
|
||||||
|
// If the user disabled direct messages, try the fallback (if defined)
|
||||||
|
if (message.asOption().isEmpty() && hasFallback()) {
|
||||||
|
fallbackChannel!!.sendMessage(
|
||||||
|
fallbackMessage!!.replace("@@", event.user.mentionTag),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val fallbackMessage: String? = rawWelcome.fallbackMessage
|
}
|
||||||
}
|
|
||||||
|
override fun handle(message: MessageCreateEvent) {
|
||||||
|
embed?.let {
|
||||||
|
MessageUtil.sendEmbed(message.channel, it)
|
||||||
|
} ?: Log.info("Welcome command was triggered, but no welcome embed defined.")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasFallback(): Boolean = fallbackChannel != null && fallbackMessage != null
|
||||||
|
|
||||||
|
private val fallbackChannel: TextChannel? = fallbackChannel?.let { channel ->
|
||||||
|
requireNotNull(fallbackMessage) {
|
||||||
|
"[feature.welcome.fallbackMessage] must not be null if fallbackChannel is defined"
|
||||||
|
}
|
||||||
|
Util.findChannel(channel).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package moe.kageru.kagebot.persistence
|
||||||
|
|
||||||
|
import arrow.core.k
|
||||||
|
import org.mapdb.DBMaker
|
||||||
|
import org.mapdb.Serializer
|
||||||
|
|
||||||
|
object Dao {
|
||||||
|
private val db = DBMaker.fileDB("kagebot.db").fileMmapEnable().transactionEnable().make()
|
||||||
|
private val prisoners = db.hashMap("timeout", Serializer.LONG, Serializer.LONG_ARRAY).createOrOpen()
|
||||||
|
private val commands = db.hashMap("commands", Serializer.STRING, Serializer.INTEGER).createOrOpen()
|
||||||
|
private val tempVcs = db.hashSet("vcs", Serializer.STRING).createOrOpen()
|
||||||
|
|
||||||
|
fun saveTimeout(releaseTime: Long, user: Long, roles: List<Long>) {
|
||||||
|
prisoners[releaseTime] = (listOf(user) + roles).toLongArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCommandCounter(count: Int) {
|
||||||
|
commands["total"] = count
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCommandCounter() = commands["total"] ?: 0
|
||||||
|
|
||||||
|
fun close() = db.close()
|
||||||
|
|
||||||
|
fun getAllTimeouts() = prisoners.keys.k()
|
||||||
|
|
||||||
|
fun deleteTimeout(releaseTime: Long): List<Long> {
|
||||||
|
val timeout = prisoners[releaseTime]!!
|
||||||
|
prisoners.remove(releaseTime)
|
||||||
|
return timeout.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTemporaryVC(channel: String): Boolean {
|
||||||
|
return channel in tempVcs
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addTemporaryVC(channel: String) {
|
||||||
|
tempVcs.add(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeTemporaryVC(channel: String) {
|
||||||
|
tempVcs.remove(channel)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,23 +8,27 @@ permissionDenied = "You do not have permission to use this command."
|
||||||
# results in <name> says <message>
|
# results in <name> says <message>
|
||||||
redirectedMessage = "says"
|
redirectedMessage = "says"
|
||||||
messageDeleted = "Your message was deleted because it contained a banned word or phrase."
|
messageDeleted = "Your message was deleted because it contained a banned word or phrase."
|
||||||
|
# @@ will be replaced with the time
|
||||||
|
timeout = "You have been timed out for @@ minutes"
|
||||||
|
|
||||||
# If this is enabled, every new user will receive a welcome message.
|
# If this is enable, every new user will receive a welcome message.
|
||||||
# If the user has disabled their DMs, the fallbackMessage will be sent in the fallbackChannel instead.
|
# If the user has disabled their DMs, the fallbackMessage will be sent in the fallbackChannel instead.
|
||||||
# If no fallback channel or message is specified, no fallback will be sent.
|
# If no fallback channel or message is specified, no fallback will be sent.
|
||||||
[feature.welcome]
|
[feature.welcome]
|
||||||
enabled = true
|
|
||||||
fallbackChannel = "555097559023222825"
|
fallbackChannel = "555097559023222825"
|
||||||
fallbackMessage = "@@ I would like to greet you, but I can’t. :("
|
fallbackMessage = "@@ I would like to greet you, but I can’t. :("
|
||||||
# This is a list of pairs where the key is the title and the value the content of the paragraph.
|
# This is a list of strings like [title1, content1, title2, content2, ...]
|
||||||
# Do not use empty strings to get empty headings or paragraphs. The discord API rejects those.
|
# Do not use empty strings to get empty headings or paragraphs. The discord API rejects those.
|
||||||
[feature.welcome.content]
|
content = [
|
||||||
"Welcome to the Server" = "This is the content of the first paragraph"
|
"Welcome to the Server" , "This is the content of the first paragraph",
|
||||||
"Second paragraph heading" = "Second paragraph content"
|
"Second paragraph heading", "Second paragraph content",
|
||||||
|
"3rd", "aoisd",
|
||||||
|
"fourth", "asasd",
|
||||||
|
"5th", "asdasd"
|
||||||
|
]
|
||||||
|
|
||||||
# allow the bot owner to get debug stats
|
[feature.timeout]
|
||||||
[feature.debug]
|
role = "timeout"
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[[command]]
|
[[command]]
|
||||||
trigger = "!ping"
|
trigger = "!ping"
|
||||||
|
@ -40,6 +44,10 @@ trigger = "A.+B"
|
||||||
response = "regex matched"
|
response = "regex matched"
|
||||||
matchType = "REGEX"
|
matchType = "REGEX"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!embed"
|
||||||
|
embed = [ "some embed heading", "your embed content" ]
|
||||||
|
|
||||||
[[command]]
|
[[command]]
|
||||||
trigger = "answer me"
|
trigger = "answer me"
|
||||||
# this will @mention the user who triggered the command,
|
# this will @mention the user who triggered the command,
|
||||||
|
@ -56,15 +64,15 @@ trigger = "!restricted"
|
||||||
response = "access granted"
|
response = "access granted"
|
||||||
[command.permissions]
|
[command.permissions]
|
||||||
hasOneOf = [
|
hasOneOf = [
|
||||||
"452034011393425409",
|
"new role",
|
||||||
"446668543816106004"
|
"another new role"
|
||||||
]
|
]
|
||||||
|
|
||||||
[[command]]
|
[[command]]
|
||||||
trigger = "!almostUnrestricted"
|
trigger = "!almostUnrestricted"
|
||||||
response = "access granted"
|
response = "access granted"
|
||||||
[command.permissions]
|
[command.permissions]
|
||||||
hasNoneOf = ["452034011393425409"]
|
hasNoneOf = ["new role"]
|
||||||
|
|
||||||
[[command]]
|
[[command]]
|
||||||
trigger = "!private"
|
trigger = "!private"
|
||||||
|
@ -72,7 +80,7 @@ response = "some long response that you don’t want in public channels"
|
||||||
[command.permissions]
|
[command.permissions]
|
||||||
onlyDM = true
|
onlyDM = true
|
||||||
|
|
||||||
# redirect every message that starts with !redirect to channel 555097559023222825
|
# redirect every message that starts with !redirect to a channel called “testchannel”
|
||||||
[[command]]
|
[[command]]
|
||||||
trigger = "!redirect"
|
trigger = "!redirect"
|
||||||
response = "redirected"
|
response = "redirected"
|
||||||
|
@ -84,5 +92,38 @@ target = "testchannel"
|
||||||
trigger = "!anonRedirect"
|
trigger = "!anonRedirect"
|
||||||
response = "redirected"
|
response = "redirected"
|
||||||
[command.action.redirect]
|
[command.action.redirect]
|
||||||
target = "555097559023222825"
|
target = "testchannel"
|
||||||
anonymous = true
|
anonymous = true
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!assign"
|
||||||
|
[command.action.assign]
|
||||||
|
role = "new role"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!debug"
|
||||||
|
feature = "debug"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!welcome"
|
||||||
|
feature = "welcome"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!help"
|
||||||
|
feature = "help"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!getConfig"
|
||||||
|
feature = "getConfig"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!setConfig"
|
||||||
|
feature = "setConfig"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!prison"
|
||||||
|
feature = "timeout"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!vc"
|
||||||
|
feature = "vc"
|
||||||
|
|
|
@ -3,11 +3,41 @@ package moe.kageru.kagebot
|
||||||
import io.kotlintest.shouldBe
|
import io.kotlintest.shouldBe
|
||||||
import io.kotlintest.shouldNotBe
|
import io.kotlintest.shouldNotBe
|
||||||
import io.kotlintest.specs.StringSpec
|
import io.kotlintest.specs.StringSpec
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.config.LocalizationSpec
|
||||||
|
import moe.kageru.kagebot.config.SystemSpec
|
||||||
|
import moe.kageru.kagebot.features.SetConfigFeature
|
||||||
|
import java.awt.Color
|
||||||
|
|
||||||
class ConfigTest : StringSpec({
|
@ExperimentalStdlibApi
|
||||||
TestUtil.prepareTestEnvironment()
|
class ConfigTest : StringSpec() {
|
||||||
|
init {
|
||||||
"should properly parse test config" {
|
"should properly parse test config" {
|
||||||
Globals.config shouldNotBe null
|
TestUtil.prepareTestEnvironment()
|
||||||
Globals.config.commands shouldBe emptyList()
|
Config.system[SystemSpec.serverId] shouldNotBe null
|
||||||
|
SystemSpec.color shouldBe Color.decode("#1793d0")
|
||||||
|
Config.features.welcome!!.embed shouldNotBe null
|
||||||
|
Config.commands.size shouldBe 3
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
"should parse test config via command" {
|
||||||
|
val redir = "says"
|
||||||
|
val testConfig = """
|
||||||
|
[localization]
|
||||||
|
redirectedMessage = "$redir"
|
||||||
|
messageDeleted = "dongered"
|
||||||
|
timeout = "timeout"
|
||||||
|
""".trimIndent()
|
||||||
|
val message = TestUtil.mockMessage("anything")
|
||||||
|
every { message.messageAttachments } returns listOf(
|
||||||
|
mockk {
|
||||||
|
every { url.openStream().readAllBytes() } returns testConfig.toByteArray()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SetConfigFeature().handle(message)
|
||||||
|
Config.localization[LocalizationSpec.redirectedMessage] shouldBe redir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,103 +1,152 @@
|
||||||
package moe.kageru.kagebot
|
package moe.kageru.kagebot
|
||||||
|
|
||||||
|
import arrow.core.ListK
|
||||||
|
import arrow.core.Option
|
||||||
|
import io.kotlintest.matchers.string.shouldContain
|
||||||
|
import io.kotlintest.matchers.string.shouldNotContain
|
||||||
import io.kotlintest.shouldBe
|
import io.kotlintest.shouldBe
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import moe.kageru.kagebot.Kagebot.process
|
||||||
import moe.kageru.kagebot.config.Config
|
import moe.kageru.kagebot.config.Config
|
||||||
import moe.kageru.kagebot.config.RawConfig
|
import moe.kageru.kagebot.config.ConfigParser
|
||||||
import org.javacord.api.DiscordApi
|
import moe.kageru.kagebot.extensions.*
|
||||||
import org.javacord.api.entity.channel.ServerTextChannel
|
import org.javacord.api.entity.channel.ServerTextChannel
|
||||||
import org.javacord.api.entity.message.embed.EmbedBuilder
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
|
import org.javacord.api.entity.permission.Role
|
||||||
import org.javacord.api.entity.user.User
|
import org.javacord.api.entity.user.User
|
||||||
import org.javacord.api.event.message.MessageCreateEvent
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
import org.javacord.core.entity.message.embed.EmbedBuilderDelegateImpl
|
import org.javacord.core.entity.message.embed.EmbedBuilderDelegateImpl
|
||||||
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object TestUtil {
|
object TestUtil {
|
||||||
fun mockMessage(
|
private val TIMEOUT_ROLE = mockk<Role> {
|
||||||
content: String,
|
every { id } returns 123
|
||||||
replies: MutableList<String> = mutableListOf(),
|
}
|
||||||
replyEmbeds: MutableList<EmbedBuilder> = mutableListOf(),
|
val TEST_ROLE: Role = mockk {
|
||||||
isBot: Boolean = false
|
every { id } returns 1
|
||||||
): MessageCreateEvent {
|
every { isManaged } returns false
|
||||||
return mockk {
|
every { name } returns "testrole"
|
||||||
every { messageContent } returns content
|
}
|
||||||
every { readableMessageContent } returns content
|
|
||||||
every { channel.sendMessage(capture(replies)) } returns mockk()
|
fun mockMessage(
|
||||||
every { channel.sendMessage(capture(replyEmbeds)) } returns mockk()
|
content: String,
|
||||||
every { message.canYouDelete() } returns true
|
replies: MutableList<String> = mutableListOf(),
|
||||||
every { isPrivateMessage } returns false
|
replyEmbeds: MutableList<EmbedBuilder> = mutableListOf(),
|
||||||
// We can’t use a nested mock here because other fields of messageAuthor might
|
files: MutableList<File> = mutableListOf(),
|
||||||
// get overwritten by other tests, which would delete a nested mock.
|
isBot: Boolean = false,
|
||||||
every { messageAuthor.id } returns 1
|
): MessageCreateEvent {
|
||||||
every { messageAuthor.discriminatedName } returns "testuser#1234"
|
return mockk {
|
||||||
every { messageAuthor.isBotUser } returns isBot
|
every { messageContent } returns content
|
||||||
every { messageAuthor.isYourself } returns isBot
|
every { readableMessageContent } returns content
|
||||||
every { messageAuthor.isBotOwner } returns false
|
every { channel.sendMessage(capture(replies)) } returns mockk(relaxed = true) {
|
||||||
every { messageAuthor.asUser() } returns Optional.of(messageableAuthor())
|
every { isCompletedExceptionally } returns false
|
||||||
|
}
|
||||||
|
every { channel.sendMessage(capture(replyEmbeds)) } returns mockk(relaxed = true) {
|
||||||
|
every { isCompletedExceptionally } returns false
|
||||||
|
}
|
||||||
|
every { channel.sendMessage(capture(files)) } returns mockk(relaxed = true) {
|
||||||
|
every { isCompletedExceptionally } returns false
|
||||||
|
}
|
||||||
|
every { message.canYouDelete() } returns true
|
||||||
|
every { isPrivateMessage } returns false
|
||||||
|
// We can’t use a nested mock here because other fields of messageAuthor might
|
||||||
|
// get overwritten by other tests, which would delete a nested mock.
|
||||||
|
every { messageAuthor.id } returns 1
|
||||||
|
every { messageAuthor.discriminatedName } returns "testuser#1234"
|
||||||
|
every { messageAuthor.isBotUser } returns isBot
|
||||||
|
every { messageAuthor.isYourself } returns isBot
|
||||||
|
every { messageAuthor.isBotOwner } returns false
|
||||||
|
every { messageAuthor.asUser() } returns Optional.of(messageableAuthor(replyEmbeds))
|
||||||
|
every { messageAuthor.name } returns "kageru"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun messageableAuthor(messages: MutableList<EmbedBuilder> = mutableListOf()): User {
|
||||||
|
return mockk {
|
||||||
|
every { roles() } returns ListK.empty()
|
||||||
|
every { sendMessage(capture(messages)) } returns mockk(relaxed = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun prepareTestEnvironment(
|
||||||
|
sentEmbeds: MutableList<EmbedBuilder> = mutableListOf(),
|
||||||
|
sentMessages: MutableList<String> = mutableListOf(),
|
||||||
|
dmEmbeds: MutableList<EmbedBuilder> = mutableListOf(),
|
||||||
|
) {
|
||||||
|
val channel = mockk<ServerTextChannel>(relaxed = true) {
|
||||||
|
every { sendMessage(capture(sentEmbeds)) } returns mockk(relaxed = true) {
|
||||||
|
every { join() } returns mockk {
|
||||||
|
every { isCompletedExceptionally } returns false
|
||||||
}
|
}
|
||||||
|
every { isCompletedExceptionally } returns false
|
||||||
|
}
|
||||||
|
every { sendMessage(capture(sentMessages)) } returns mockk(relaxed = true)
|
||||||
}
|
}
|
||||||
|
// mockk tries to access Config.server in the mocking block below, so we need to provide some kind of value
|
||||||
|
Config.server = mockk()
|
||||||
|
Config.server = mockk(relaxed = true) {
|
||||||
|
every { icon.ifPresent(any()) } just Runs
|
||||||
|
every { channelById(any()) } returns Option.just(channel)
|
||||||
|
every { channelsByName(any()) } returns ListK.just(channel)
|
||||||
|
every { rolesByName("testrole") } returns ListK.just(TEST_ROLE)
|
||||||
|
every { rolesByName("timeout") } returns ListK.just(TIMEOUT_ROLE)
|
||||||
|
every { categoriesByName(any()) } returns ListK.just(mockk())
|
||||||
|
every { createVoiceChannelBuilder().create() } returns mockk {
|
||||||
|
every { isCompletedExceptionally } returns false
|
||||||
|
every { join().idAsString } returns "12345"
|
||||||
|
}
|
||||||
|
every { getMembersByName(any()) } returns setOf(
|
||||||
|
mockk(relaxed = true) {
|
||||||
|
every { id } returns 123
|
||||||
|
every { roles() } returns ListK.just(TEST_ROLE)
|
||||||
|
every { getRoles(any()) } returns ListK.just(TEST_ROLE)
|
||||||
|
every { sendMessage(capture(dmEmbeds)) } returns mockk(relaxed = true) {
|
||||||
|
every { isCompletedExceptionally } returns false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Globals.api = mockk(relaxed = true) {
|
||||||
|
every { getServerById(any<String>()) } returns Optional.of(Config.server)
|
||||||
|
}
|
||||||
|
ConfigParser.initialLoad("testconfig.toml")
|
||||||
|
}
|
||||||
|
|
||||||
fun messageableAuthor(messages: MutableList<EmbedBuilder> = mutableListOf()): User {
|
fun testMessageSuccess(content: String, result: String) {
|
||||||
return mockk {
|
val calls = mutableListOf<String>()
|
||||||
every { getRoles(any()) } returns emptyList()
|
mockMessage(content, replies = calls).process()
|
||||||
every { sendMessage(capture(messages)) } returns mockk()
|
calls shouldBe mutableListOf(result)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun prepareTestEnvironment(
|
fun embedToString(embed: EmbedBuilder): String {
|
||||||
sentEmbeds: MutableList<EmbedBuilder> = mutableListOf(),
|
return (embed.delegate as EmbedBuilderDelegateImpl).toJsonNode().toString()
|
||||||
sentMessages: MutableList<String> = mutableListOf()
|
}
|
||||||
) {
|
|
||||||
val channel = mockk<Optional<ServerTextChannel>> {
|
|
||||||
every { isPresent } returns true
|
|
||||||
every { get() } returns mockk {
|
|
||||||
every { sendMessage(capture(sentEmbeds)) } returns mockk {
|
|
||||||
every { join() } returns mockk()
|
|
||||||
every { isCompletedExceptionally } returns false
|
|
||||||
}
|
|
||||||
every { sendMessage(capture(sentMessages)) } returns mockk()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val api = mockk<DiscordApi> {
|
|
||||||
every { getServerById(any<String>()) } returns Optional.of(mockk {
|
|
||||||
every { icon.ifPresent(any()) } just Runs
|
|
||||||
every { getTextChannelById(any<String>()) } returns channel
|
|
||||||
every { getTextChannelsByName(any()) } returns listOf(channel.get())
|
|
||||||
every { getRolesByNameIgnoreCase("testrole") } returns listOf(mockk {
|
|
||||||
every { id } returns 1
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Globals.api = api
|
|
||||||
Globals.config = Config(RawConfig.read("testconfig.toml"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testMessageSuccess(content: String, result: String) {
|
fun withCommands(config: String, test: (() -> Unit)) {
|
||||||
val calls = mutableListOf<String>()
|
val oldCmds = Config.commandConfig
|
||||||
Kagebot.processMessage(mockMessage(content, replies = calls))
|
Config.commandConfig = Config.commandSpec.string(config)
|
||||||
calls shouldBe mutableListOf(result)
|
test()
|
||||||
}
|
Config.commandConfig = oldCmds
|
||||||
|
}
|
||||||
|
|
||||||
fun embedToString(embed: EmbedBuilder): String {
|
fun withReplyContents(
|
||||||
return (embed.delegate as EmbedBuilderDelegateImpl).toJsonNode().toString()
|
expected: List<String> = emptyList(),
|
||||||
|
unexpected: List<String> = emptyList(),
|
||||||
|
op: (MutableList<EmbedBuilder>) -> Unit,
|
||||||
|
) {
|
||||||
|
val replies = mutableListOf<EmbedBuilder>()
|
||||||
|
op(replies)
|
||||||
|
replies.size shouldBe 1
|
||||||
|
val replyString = embedToString(replies[0])
|
||||||
|
for (string in expected) {
|
||||||
|
replyString shouldContain string
|
||||||
}
|
}
|
||||||
|
for (string in unexpected) {
|
||||||
fun <R> withCommands(config: String, test: (() -> R)) {
|
replyString shouldNotContain string
|
||||||
val oldCmds = Globals.config.commands
|
|
||||||
val rawConfig = RawConfig.readFromString(config)
|
|
||||||
Globals.config.reloadCommands(rawConfig)
|
|
||||||
test()
|
|
||||||
Globals.config.commands = oldCmds
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fun <R> withLocalization(config: String, test: (() -> R)) {
|
}
|
||||||
val oldLoc = Globals.config.localization
|
|
||||||
val rawConfig = RawConfig.readFromString(config)
|
|
||||||
Globals.config.reloadLocalization(rawConfig.localization!!)
|
|
||||||
test()
|
|
||||||
Globals.config.localization = oldLoc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,243 +1,312 @@
|
||||||
package moe.kageru.kagebot.command
|
package moe.kageru.kagebot.command
|
||||||
|
|
||||||
|
import arrow.core.ListK
|
||||||
import io.kotlintest.matchers.string.shouldContain
|
import io.kotlintest.matchers.string.shouldContain
|
||||||
import io.kotlintest.shouldBe
|
import io.kotlintest.shouldBe
|
||||||
import io.kotlintest.specs.StringSpec
|
import io.kotlintest.specs.StringSpec
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
|
||||||
import moe.kageru.kagebot.Globals
|
import moe.kageru.kagebot.Globals
|
||||||
import moe.kageru.kagebot.Globals.config
|
import moe.kageru.kagebot.Kagebot.process
|
||||||
import moe.kageru.kagebot.Kagebot
|
|
||||||
import moe.kageru.kagebot.TestUtil
|
import moe.kageru.kagebot.TestUtil
|
||||||
import moe.kageru.kagebot.TestUtil.embedToString
|
import moe.kageru.kagebot.TestUtil.embedToString
|
||||||
import moe.kageru.kagebot.TestUtil.messageableAuthor
|
import moe.kageru.kagebot.TestUtil.messageableAuthor
|
||||||
import moe.kageru.kagebot.TestUtil.mockMessage
|
import moe.kageru.kagebot.TestUtil.mockMessage
|
||||||
|
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.TestUtil.withLocalization
|
import moe.kageru.kagebot.Util
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.extensions.roles
|
||||||
|
import moe.kageru.kagebot.extensions.rolesByName
|
||||||
|
import moe.kageru.kagebot.extensions.unwrap
|
||||||
|
import moe.kageru.kagebot.persistence.Dao
|
||||||
import org.javacord.api.entity.message.embed.EmbedBuilder
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
|
import org.javacord.api.entity.permission.Role
|
||||||
|
import org.javacord.api.entity.user.User
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class CommandTest : StringSpec({
|
class CommandTest : StringSpec({
|
||||||
TestUtil.prepareTestEnvironment()
|
prepareTestEnvironment()
|
||||||
"should match prefix command" {
|
"should increment command counter" {
|
||||||
withCommands(
|
withCommands(
|
||||||
"""
|
"""
|
||||||
[[command]]
|
[[command]]
|
||||||
trigger = "!ping"
|
trigger = "!ping"
|
||||||
response = "pong"
|
response = "pong"
|
||||||
""".trimIndent()
|
""".trimIndent(),
|
||||||
) {
|
) {
|
||||||
testMessageSuccess("!ping", "pong")
|
val before = Globals.commandCounter.get()
|
||||||
}
|
testMessageSuccess("!ping", "pong")
|
||||||
|
Globals.commandCounter.get() shouldBe (before + 1)
|
||||||
}
|
}
|
||||||
"should match contains command" {
|
}
|
||||||
withCommands(
|
"should match prefix command" {
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "somewhere"
|
[[command]]
|
||||||
response = "found it"
|
trigger = "!ping"
|
||||||
matchType = "CONTAINS"
|
response = "pong"
|
||||||
""".trimIndent()
|
""".trimIndent(),
|
||||||
) {
|
) {
|
||||||
testMessageSuccess("the trigger is somewhere in this message", "found it")
|
testMessageSuccess("!ping", "pong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"should match regex command" {
|
}
|
||||||
withCommands(
|
"should print embed for command" {
|
||||||
"""
|
val calls = mutableListOf<EmbedBuilder>()
|
||||||
[[command]]
|
prepareTestEnvironment(calls)
|
||||||
trigger = "A.+B"
|
val heading = "heading 1"
|
||||||
response = "regex matched"
|
val content = "this is the first paragraph of the embed"
|
||||||
matchType = "REGEX"
|
withCommands(
|
||||||
""".trimIndent()
|
"""
|
||||||
) {
|
[[command]]
|
||||||
testMessageSuccess("AcsdB", "regex matched")
|
trigger = "!embed"
|
||||||
}
|
embed = [ "$heading", "$content" ]
|
||||||
|
""".trimIndent(),
|
||||||
|
) {
|
||||||
|
TestUtil.withReplyContents(expected = listOf(heading, content)) {
|
||||||
|
mockMessage("!embed", replyEmbeds = it).process()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"should ping author" {
|
}
|
||||||
withCommands(
|
"should match contains command" {
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "answer me"
|
[[command]]
|
||||||
response = "@@ there you go"
|
trigger = "somewhere"
|
||||||
""".trimIndent()
|
response = "found it"
|
||||||
) {
|
matchType = "CONTAINS"
|
||||||
testMessageSuccess("answer me", "<@1> there you go")
|
""".trimIndent(),
|
||||||
}
|
) {
|
||||||
|
testMessageSuccess("the trigger is somewhere in this message", "found it")
|
||||||
}
|
}
|
||||||
"should not react to own message" {
|
}
|
||||||
withCommands(
|
"should match regex command" {
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "!ping"
|
[[command]]
|
||||||
response = "pong"
|
trigger = "A.+B"
|
||||||
""".trimIndent()
|
response = "regex matched"
|
||||||
) {
|
matchType = "REGEX"
|
||||||
val calls = mutableListOf<String>()
|
""".trimIndent(),
|
||||||
Kagebot.processMessage(mockMessage("!ping", replies = calls, isBot = true))
|
) {
|
||||||
calls shouldBe mutableListOf()
|
testMessageSuccess("AcsdB", "regex matched")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"should delete messages and send copy to author" {
|
}
|
||||||
withCommands(
|
"should ping author" {
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "delet this"
|
[[command]]
|
||||||
[command.action]
|
trigger = "answer me"
|
||||||
delete = true
|
response = "@@ there you go"
|
||||||
""".trimIndent()
|
""".trimIndent(),
|
||||||
) {
|
) {
|
||||||
val replies = mutableListOf<EmbedBuilder>()
|
testMessageSuccess("answer me", "<@1> there you go")
|
||||||
val messageContent = "delet this"
|
|
||||||
val mockMessage = mockMessage(messageContent)
|
|
||||||
every { mockMessage.deleteMessage() } returns mockk()
|
|
||||||
every { mockMessage.messageAuthor.asUser() } returns Optional.of(messageableAuthor(replies))
|
|
||||||
Kagebot.processMessage(mockMessage)
|
|
||||||
verify(exactly = 1) { mockMessage.deleteMessage() }
|
|
||||||
replies.size shouldBe 1
|
|
||||||
embedToString(replies[0]) shouldContain messageContent
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"should refuse command without permissions" {
|
}
|
||||||
withCommands(
|
"should not react to own message" {
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "!restricted"
|
[[command]]
|
||||||
response = "access granted"
|
trigger = "!ping"
|
||||||
[command.permissions]
|
response = "pong"
|
||||||
hasOneOf = [
|
""".trimIndent(),
|
||||||
"testrole",
|
) {
|
||||||
]
|
val calls = mutableListOf<String>()
|
||||||
""".trimIndent()
|
mockMessage("!ping", replies = calls, isBot = true).process()
|
||||||
) {
|
calls shouldBe mutableListOf()
|
||||||
val replies = mutableListOf<String>()
|
|
||||||
val mockMessage = mockMessage("!restricted", replies = replies)
|
|
||||||
Kagebot.processMessage(mockMessage)
|
|
||||||
replies shouldBe mutableListOf(config.localization.permissionDenied)
|
|
||||||
withLocalization(
|
|
||||||
"""
|
|
||||||
[localization]
|
|
||||||
permissionDenied = ""
|
|
||||||
messageDeleted = "whatever"
|
|
||||||
redirectedMessage = "asdja"
|
|
||||||
""".trimIndent()
|
|
||||||
) {
|
|
||||||
Kagebot.processMessage(mockMessage)
|
|
||||||
// still one string in there from earlier, nothing new was added
|
|
||||||
replies.size shouldBe 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"should accept restricted command for owner" {
|
}
|
||||||
withCommands(
|
"should delete messages and send copy to author" {
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "!restricted"
|
[[command]]
|
||||||
response = "access granted"
|
trigger = "delet this"
|
||||||
[command.permissions]
|
[command.action]
|
||||||
hasOneOf = [
|
delete = true
|
||||||
"testrole"
|
""".trimIndent(),
|
||||||
]
|
) {
|
||||||
""".trimIndent()
|
val messageContent = "delet this"
|
||||||
) {
|
TestUtil.withReplyContents(expected = listOf(messageContent)) {
|
||||||
val calls = mutableListOf<String>()
|
val mockMessage = mockMessage(messageContent)
|
||||||
val mockMessage = mockMessage("!restricted", replies = calls)
|
every { mockMessage.deleteMessage() } returns mockk()
|
||||||
every { mockMessage.messageAuthor.isBotOwner } returns true
|
every { mockMessage.messageAuthor.asUser() } returns Optional.of(messageableAuthor(it))
|
||||||
Kagebot.processMessage(mockMessage)
|
mockMessage.process()
|
||||||
calls shouldBe mutableListOf("access granted")
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"should accept restricted command with permissions" {
|
}
|
||||||
withCommands(
|
"should refuse command without permissions" {
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "!restricted"
|
[[command]]
|
||||||
response = "access granted"
|
trigger = "!restricted"
|
||||||
[command.permissions]
|
response = "access granted"
|
||||||
hasOneOf = [
|
[command.permissions]
|
||||||
"testrole"
|
hasOneOf = [
|
||||||
]
|
"testrole",
|
||||||
""".trimIndent()
|
]
|
||||||
) {
|
""".trimIndent(),
|
||||||
val calls = mutableListOf<String>()
|
) {
|
||||||
val mockMessage = mockMessage("!restricted", replies = calls)
|
val replies = mutableListOf<String>()
|
||||||
every { mockMessage.messageAuthor.asUser() } returns Optional.of(mockk {
|
val mockMessage = mockMessage("!restricted", replies = replies)
|
||||||
every { getRoles(any()) } returns listOf(
|
mockMessage.process()
|
||||||
Globals.server.getRolesByNameIgnoreCase("testrole")[0]
|
replies shouldBe mutableListOf()
|
||||||
)
|
|
||||||
})
|
|
||||||
Kagebot.processMessage(mockMessage)
|
|
||||||
calls shouldBe mutableListOf("access granted")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"should deny command to excluded roles" {
|
}
|
||||||
withCommands(
|
"should accept restricted command for owner" {
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "!almostUnrestricted"
|
[[command]]
|
||||||
response = "access granted"
|
trigger = "!restricted"
|
||||||
[command.permissions]
|
response = "access granted"
|
||||||
hasNoneOf = ["testrole"]
|
[command.permissions]
|
||||||
""".trimIndent()
|
hasOneOf = [
|
||||||
) {
|
"testrole"
|
||||||
val calls = mutableListOf<String>()
|
]
|
||||||
val mockMessage = mockMessage("!almostUnrestricted", replies = calls)
|
""".trimIndent(),
|
||||||
// with the banned role
|
) {
|
||||||
every { mockMessage.messageAuthor.asUser() } returns mockk {
|
val calls = mutableListOf<String>()
|
||||||
every { isPresent } returns true
|
val mockMessage = mockMessage("!restricted", replies = calls)
|
||||||
every { get().getRoles(any()) } returns listOf(
|
every { mockMessage.messageAuthor.isBotOwner } returns true
|
||||||
Globals.server.getRolesByNameIgnoreCase("testrole")[0]
|
mockMessage.process()
|
||||||
)
|
calls shouldBe mutableListOf("access granted")
|
||||||
}
|
}
|
||||||
Kagebot.processMessage(mockMessage)
|
}
|
||||||
|
"should accept restricted command with permissions" {
|
||||||
|
withCommands(
|
||||||
|
"""
|
||||||
|
[[command]]
|
||||||
|
trigger = "!restricted"
|
||||||
|
response = "access granted"
|
||||||
|
[command.permissions]
|
||||||
|
hasOneOf = [
|
||||||
|
"testrole"
|
||||||
|
]
|
||||||
|
""".trimIndent(),
|
||||||
|
) {
|
||||||
|
val calls = mutableListOf<String>()
|
||||||
|
val mockMessage = mockMessage("!restricted", replies = calls)
|
||||||
|
every { mockMessage.messageAuthor.asUser() } returns Optional.of(
|
||||||
|
mockk {
|
||||||
|
every { roles() } returns ListK.just(
|
||||||
|
Config.server.rolesByName("testrole").first(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
mockMessage.process()
|
||||||
|
calls shouldBe mutableListOf("access granted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"should deny command to excluded roles" {
|
||||||
|
withCommands(
|
||||||
|
"""
|
||||||
|
[[command]]
|
||||||
|
trigger = "!almostUnrestricted"
|
||||||
|
response = "access granted"
|
||||||
|
[command.permissions]
|
||||||
|
hasNoneOf = ["testrole"]
|
||||||
|
""".trimIndent(),
|
||||||
|
) {
|
||||||
|
val calls = mutableListOf<String>()
|
||||||
|
val mockMessage = mockMessage("!almostUnrestricted", replies = calls)
|
||||||
|
// with the banned role
|
||||||
|
every { mockMessage.messageAuthor.asUser() } returns mockk {
|
||||||
|
every { isPresent } returns true
|
||||||
|
every { get().getRoles(any()) } returns listOf(
|
||||||
|
Config.server.rolesByName("testrole").first(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mockMessage.process()
|
||||||
|
|
||||||
// without the role
|
// without the role
|
||||||
every { mockMessage.messageAuthor.asUser() } returns mockk {
|
every { mockMessage.messageAuthor.asUser() } returns mockk {
|
||||||
every { isPresent } returns true
|
every { isPresent } returns true
|
||||||
every { get().getRoles(any()) } returns emptyList()
|
every { get().getRoles(any()) } returns emptyList()
|
||||||
}
|
}
|
||||||
Kagebot.processMessage(mockMessage)
|
mockMessage.process()
|
||||||
calls shouldBe mutableListOf(config.localization.permissionDenied, "access granted")
|
// first message didn’t answer anything
|
||||||
}
|
calls shouldBe mutableListOf("access granted")
|
||||||
}
|
}
|
||||||
"should refuse DM only message in server channel" {
|
}
|
||||||
withCommands(
|
"should refuse DM only message in server channel" {
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "!dm"
|
[[command]]
|
||||||
response = "access granted"
|
trigger = "!dm"
|
||||||
[command.permissions]
|
response = "access granted"
|
||||||
onlyDM = true
|
[command.permissions]
|
||||||
""".trimIndent()
|
onlyDM = true
|
||||||
) {
|
""".trimIndent(),
|
||||||
val calls = mutableListOf<String>()
|
) {
|
||||||
Kagebot.processMessage(mockMessage("!dm", replies = calls))
|
val calls = mutableListOf<String>()
|
||||||
calls shouldBe listOf(config.localization.permissionDenied)
|
mockMessage("!dm", replies = calls).process()
|
||||||
}
|
calls shouldBe mutableListOf()
|
||||||
}
|
}
|
||||||
/*
|
}
|
||||||
* This implicitly tests that the message author is not included in anonymous complaints
|
/*
|
||||||
* because getting the author’s name from the mock is undefined.
|
* This implicitly tests that the message author is not included in anonymous complaints
|
||||||
*/
|
* because getting the author’s name from the mock is undefined.
|
||||||
"should redirect" {
|
*/
|
||||||
val calls = mutableListOf<EmbedBuilder>()
|
"should redirect" {
|
||||||
TestUtil.prepareTestEnvironment(calls)
|
val calls = mutableListOf<EmbedBuilder>()
|
||||||
withCommands(
|
prepareTestEnvironment(calls)
|
||||||
"""
|
withCommands(
|
||||||
[[command]]
|
"""
|
||||||
trigger = "!redirect"
|
[[command]]
|
||||||
response = "redirected"
|
trigger = "!redirect"
|
||||||
[command.action.redirect]
|
response = "redirected"
|
||||||
target = "testchannel"
|
[command.action.redirect]
|
||||||
anonymous = true
|
target = "testchannel"
|
||||||
""".trimIndent()
|
anonymous = true
|
||||||
) {
|
""".trimIndent(),
|
||||||
val message = "this is a message"
|
) {
|
||||||
Kagebot.processMessage(mockMessage("!redirect $message", replyEmbeds = calls))
|
val message = "this is a message"
|
||||||
calls.size shouldBe 1
|
mockMessage("!redirect $message").process()
|
||||||
embedToString(calls[0]) shouldContain "\"$message\""
|
calls.size shouldBe 1
|
||||||
}
|
embedToString(calls[0]) shouldContain "\"$message\""
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
"should assign" {
|
||||||
|
withCommands(
|
||||||
|
"""
|
||||||
|
[[command]]
|
||||||
|
trigger = "!assign"
|
||||||
|
[command.action.assign]
|
||||||
|
role = "testrole"
|
||||||
|
""".trimIndent(),
|
||||||
|
) {
|
||||||
|
val roles = mutableListOf<Role>()
|
||||||
|
val user = mockk<User> {
|
||||||
|
every { addRole(capture(roles), "Requested via command.") } returns mockk()
|
||||||
|
}
|
||||||
|
every { Config.server.getMemberById(1) } returns Optional.of(user)
|
||||||
|
mockMessage("!assign").process()
|
||||||
|
roles shouldBe mutableListOf(Util.findRole("testrole").unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"should create VC" {
|
||||||
|
withCommands(
|
||||||
|
"""
|
||||||
|
[[command]]
|
||||||
|
trigger = "!vc"
|
||||||
|
feature = "vc"
|
||||||
|
""".trimIndent(),
|
||||||
|
) {
|
||||||
|
testMessageSuccess("!vc 2", "Done")
|
||||||
|
Dao.isTemporaryVC("12345") shouldBe true
|
||||||
|
Dao.removeTemporaryVC("12345")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"should reject invalid vc command" {
|
||||||
|
withCommands(
|
||||||
|
"""
|
||||||
|
[[command]]
|
||||||
|
trigger = "!vc"
|
||||||
|
feature = "vc"
|
||||||
|
""".trimIndent(),
|
||||||
|
) {
|
||||||
|
testMessageSuccess("!vc asd", "Invalid syntax, expected a number as limit, got asd")
|
||||||
|
Dao.isTemporaryVC("12345") shouldBe false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import io.kotlintest.shouldBe
|
||||||
|
import io.kotlintest.specs.ShouldSpec
|
||||||
|
import moe.kageru.kagebot.Kagebot.process
|
||||||
|
import moe.kageru.kagebot.TestUtil
|
||||||
|
import moe.kageru.kagebot.TestUtil.mockMessage
|
||||||
|
import moe.kageru.kagebot.TestUtil.withCommands
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ConfigFeatureTest : ShouldSpec({
|
||||||
|
TestUtil.prepareTestEnvironment()
|
||||||
|
"getConfig should sent message with attachment" {
|
||||||
|
withCommands(
|
||||||
|
"""
|
||||||
|
[[command]]
|
||||||
|
trigger = "!getConfig"
|
||||||
|
feature = "getConfig"
|
||||||
|
""".trimIndent(),
|
||||||
|
) {
|
||||||
|
val calls = mutableListOf<File>()
|
||||||
|
mockMessage("!getConfig", files = calls).process()
|
||||||
|
calls.size shouldBe 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -4,29 +4,29 @@ import io.kotlintest.specs.StringSpec
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import moe.kageru.kagebot.Kagebot.process
|
||||||
import moe.kageru.kagebot.TestUtil
|
import moe.kageru.kagebot.TestUtil
|
||||||
import moe.kageru.kagebot.config.RawDebugFeatures
|
|
||||||
import org.javacord.api.entity.message.embed.EmbedBuilder
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
import org.javacord.api.event.message.MessageCreateEvent
|
import org.javacord.api.event.message.MessageCreateEvent
|
||||||
|
|
||||||
class DebugFeatureTest : StringSpec({
|
class DebugFeatureTest : StringSpec({
|
||||||
TestUtil.prepareTestEnvironment()
|
TestUtil.prepareTestEnvironment()
|
||||||
// this will fail if the bot tries to execute more than it should
|
// this will fail if the bot tries to execute more than it should
|
||||||
// because the mock does not provide the necessary methods
|
// because the mock does not provide the necessary methods
|
||||||
"should ignore regular users" {
|
"should ignore regular users" {
|
||||||
val message = mockk<MessageCreateEvent> {
|
val message = TestUtil.mockMessage("!debug")
|
||||||
every { messageAuthor.isBotOwner } returns false
|
every { message.messageAuthor.isBotOwner } returns false
|
||||||
}
|
message.process()
|
||||||
DebugFeatures(RawDebugFeatures(true)).handle(message)
|
DebugFeature().handle(message)
|
||||||
verify(exactly = 0) { message.channel.sendMessage(any<EmbedBuilder>()) }
|
verify(exactly = 0) { message.channel.sendMessage(any<EmbedBuilder>()) }
|
||||||
|
}
|
||||||
|
"should return something" {
|
||||||
|
val message = mockk<MessageCreateEvent> {
|
||||||
|
every { messageAuthor.isBotOwner } returns true
|
||||||
|
every { readableMessageContent } returns "!debug"
|
||||||
|
every { channel.sendMessage(any<EmbedBuilder>()) } returns mockk()
|
||||||
}
|
}
|
||||||
"should return something" {
|
DebugFeature().handle(message)
|
||||||
val message = mockk<MessageCreateEvent> {
|
verify(exactly = 1) { message.channel.sendMessage(any<EmbedBuilder>()) }
|
||||||
every { messageAuthor.isBotOwner } returns true
|
}
|
||||||
every { readableMessageContent } returns "!debugstats something"
|
})
|
||||||
every { channel.sendMessage(any<EmbedBuilder>()) } returns mockk()
|
|
||||||
}
|
|
||||||
DebugFeatures(RawDebugFeatures(true)).handle(message)
|
|
||||||
verify(exactly = 1) { message.channel.sendMessage(any<EmbedBuilder>()) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import io.kotlintest.specs.StringSpec
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import moe.kageru.kagebot.Kagebot.process
|
||||||
|
import moe.kageru.kagebot.TestUtil
|
||||||
|
import moe.kageru.kagebot.TestUtil.mockMessage
|
||||||
|
import moe.kageru.kagebot.TestUtil.withCommands
|
||||||
|
import moe.kageru.kagebot.TestUtil.withReplyContents
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.extensions.rolesByName
|
||||||
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class HelpFeatureTest : StringSpec({
|
||||||
|
val sentEmbeds = mutableListOf<EmbedBuilder>()
|
||||||
|
TestUtil.prepareTestEnvironment(sentEmbeds = sentEmbeds)
|
||||||
|
val commandConfig = """
|
||||||
|
[[command]]
|
||||||
|
trigger = "!help"
|
||||||
|
feature = "help"
|
||||||
|
[[command]]
|
||||||
|
trigger = "!ping"
|
||||||
|
[[command]]
|
||||||
|
trigger = "!something"
|
||||||
|
[[command]]
|
||||||
|
trigger = "not a prefix"
|
||||||
|
matchType = "CONTAINS"
|
||||||
|
[[command]]
|
||||||
|
trigger = "!prison"
|
||||||
|
[command.permissions]
|
||||||
|
hasOneOf = ["testrole"]
|
||||||
|
""".trimIndent()
|
||||||
|
"should show prefix command" {
|
||||||
|
withCommands(commandConfig) {
|
||||||
|
val expected = listOf("!ping", "!something")
|
||||||
|
val unexpected = listOf("not a prefix", "!prison")
|
||||||
|
withReplyContents(expected = expected, unexpected = unexpected) { replies ->
|
||||||
|
mockMessage("!help", replyEmbeds = replies).process()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"should show moderation commands for mod" {
|
||||||
|
withCommands(commandConfig) {
|
||||||
|
val expected = listOf("!ping", "!something", "!prison")
|
||||||
|
val unexpected = listOf("not a prefix")
|
||||||
|
withReplyContents(expected = expected, unexpected = unexpected) { replies ->
|
||||||
|
val message = mockMessage("!help", replyEmbeds = replies)
|
||||||
|
every { message.messageAuthor.asUser() } returns Optional.of(
|
||||||
|
mockk {
|
||||||
|
every { getRoles(any()) } returns listOf(
|
||||||
|
Config.server.rolesByName("testrole").first(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
message.process()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,62 @@
|
||||||
|
package moe.kageru.kagebot.features
|
||||||
|
|
||||||
|
import io.kotlintest.matchers.string.shouldContain
|
||||||
|
import io.kotlintest.shouldBe
|
||||||
|
import io.kotlintest.specs.StringSpec
|
||||||
|
import moe.kageru.kagebot.Kagebot.process
|
||||||
|
import moe.kageru.kagebot.TestUtil
|
||||||
|
import moe.kageru.kagebot.TestUtil.TEST_ROLE
|
||||||
|
import moe.kageru.kagebot.persistence.Dao
|
||||||
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
|
|
||||||
|
class TimeoutFeatureTest : StringSpec({
|
||||||
|
TestUtil.prepareTestEnvironment()
|
||||||
|
"should remove and store roles" {
|
||||||
|
clearTimeouts()
|
||||||
|
TestUtil.mockMessage("!timeout kageru 99999999").process()
|
||||||
|
Dao.getAllTimeouts().let {
|
||||||
|
it.size shouldBe 1
|
||||||
|
val user = Dao.deleteTimeout(it.first())
|
||||||
|
user shouldBe arrayOf(123, TEST_ROLE.id)
|
||||||
|
}
|
||||||
|
clearTimeouts()
|
||||||
|
}
|
||||||
|
"should announce timeout via DM" {
|
||||||
|
val dms = mutableListOf<EmbedBuilder>()
|
||||||
|
TestUtil.prepareTestEnvironment(dmEmbeds = dms)
|
||||||
|
val time = "1235436"
|
||||||
|
TestUtil.mockMessage("!timeout kageru $time").process()
|
||||||
|
dms.size shouldBe 1
|
||||||
|
TestUtil.embedToString(dms[0]) shouldContain time
|
||||||
|
clearTimeouts()
|
||||||
|
}
|
||||||
|
"should return error for invalid input" {
|
||||||
|
val replies = mutableListOf<String>()
|
||||||
|
TestUtil.mockMessage("!timeout kageruWithoutATime", replies = replies).process()
|
||||||
|
replies.size shouldBe 1
|
||||||
|
replies[0] shouldContain "Error"
|
||||||
|
}
|
||||||
|
"should catch malformed time" {
|
||||||
|
val replies = mutableListOf<String>()
|
||||||
|
TestUtil.mockMessage("!timeout kageru this is not a time", replies = replies).process()
|
||||||
|
replies.size shouldBe 1
|
||||||
|
replies[0] shouldContain "Error"
|
||||||
|
}
|
||||||
|
"should print optional reason" {
|
||||||
|
val dms = mutableListOf<EmbedBuilder>()
|
||||||
|
TestUtil.prepareTestEnvironment(dmEmbeds = dms)
|
||||||
|
val reason = "because I don’t like you"
|
||||||
|
TestUtil.mockMessage("!timeout kageru 1 $reason").process()
|
||||||
|
dms.size shouldBe 1
|
||||||
|
TestUtil.embedToString(dms[0]) shouldContain reason
|
||||||
|
clearTimeouts()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
companion object {
|
||||||
|
private fun clearTimeouts() {
|
||||||
|
Dao.getAllTimeouts().forEach { to ->
|
||||||
|
Dao.deleteTimeout(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,41 +4,45 @@ import io.kotlintest.shouldBe
|
||||||
import io.kotlintest.specs.StringSpec
|
import io.kotlintest.specs.StringSpec
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import moe.kageru.kagebot.Globals
|
import io.mockk.verify
|
||||||
import moe.kageru.kagebot.Kagebot
|
|
||||||
import moe.kageru.kagebot.TestUtil
|
import moe.kageru.kagebot.TestUtil
|
||||||
|
import moe.kageru.kagebot.config.Config
|
||||||
|
import moe.kageru.kagebot.extensions.channelsByName
|
||||||
import org.javacord.api.entity.message.embed.EmbedBuilder
|
import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||||
|
|
||||||
|
@ExperimentalStdlibApi
|
||||||
class WelcomeFeatureTest : StringSpec({
|
class WelcomeFeatureTest : StringSpec({
|
||||||
TestUtil.prepareTestEnvironment()
|
TestUtil.prepareTestEnvironment()
|
||||||
"should send welcome" {
|
"should send welcome" {
|
||||||
val sentMessages = mutableListOf<EmbedBuilder>()
|
val sentMessages = mutableListOf<EmbedBuilder>()
|
||||||
Kagebot.welcomeUser(
|
Config.features.welcome!!.welcomeUser(
|
||||||
mockk {
|
mockk {
|
||||||
every { user } returns mockk {
|
every { user } returns mockk {
|
||||||
every { sendMessage(capture(sentMessages)) } returns mockk {
|
every { discriminatedName } returns "testuser#1234"
|
||||||
every { join() } returns mockk()
|
every { sendMessage(capture(sentMessages)) } returns mockk {
|
||||||
every { isCompletedExceptionally } returns false
|
every { join() } returns mockk()
|
||||||
}
|
every { isCompletedExceptionally } returns false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
sentMessages shouldBe mutableListOf(Globals.config.features.welcome!!.embed)
|
)
|
||||||
}
|
sentMessages shouldBe mutableListOf(Config.features.welcome!!.embed)
|
||||||
"should send welcome fallback if DMs are disabled" {
|
}
|
||||||
val message = mutableListOf<String>()
|
"should send welcome fallback if DMs are disabled" {
|
||||||
TestUtil.prepareTestEnvironment(sentMessages = message)
|
Config.features.welcome!!.welcomeUser(
|
||||||
Kagebot.welcomeUser(
|
mockk {
|
||||||
mockk {
|
every { user } returns mockk {
|
||||||
every { user } returns mockk {
|
every { discriminatedName } returns "testuser#1234"
|
||||||
every { id } returns 123
|
every { id } returns 123
|
||||||
every { sendMessage(any<EmbedBuilder>()) } returns mockk {
|
every { sendMessage(any<EmbedBuilder>()) } returns mockk {
|
||||||
every { join() } returns mockk()
|
every { join() } returns mockk()
|
||||||
every { isCompletedExceptionally } returns true
|
every { isCompletedExceptionally } returns true
|
||||||
}
|
}
|
||||||
}
|
every { mentionTag } returns "<@123>"
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
message shouldBe mutableListOf("<@123> welcome")
|
)
|
||||||
}
|
val channel = Config.server.channelsByName("").first()
|
||||||
|
verify(exactly = 1) { channel.sendMessage("<@123> welcome") }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,16 +3,34 @@ serverId = "356414885292277771"
|
||||||
color = "#1793d0"
|
color = "#1793d0"
|
||||||
|
|
||||||
[localization]
|
[localization]
|
||||||
permissionDenied = "no permissions"
|
|
||||||
redirectedMessage = "says"
|
redirectedMessage = "says"
|
||||||
messageDeleted = "message dongered"
|
messageDeleted = "message dongered"
|
||||||
|
timeout = "timeout @@ minutes"
|
||||||
|
|
||||||
[feature.welcome]
|
[feature.welcome]
|
||||||
enabled = true
|
|
||||||
fallbackChannel = "123"
|
fallbackChannel = "123"
|
||||||
fallbackMessage = "@@ welcome"
|
fallbackMessage = "@@ welcome"
|
||||||
# This is a list of pairs where the key is the title and the value the content of the paragraph.
|
# This is a list of pairs where the key is the title and the value the content of the paragraph.
|
||||||
# Do not use empty strings to get empty headings or paragraphs. The discord API rejects those.
|
# Do not use empty strings to get empty headings or paragraphs. The discord API rejects those.
|
||||||
[feature.welcome.content]
|
content = [
|
||||||
"Welcome to the Server" = "This is the content of the first paragraph"
|
"Welcome to the Server", "This is the content of the first paragraph",
|
||||||
"Second paragraph heading" = "Second paragraph content"
|
"Second paragraph heading", "Second paragraph content"
|
||||||
|
]
|
||||||
|
|
||||||
|
[feature.timeout]
|
||||||
|
role = "timeout"
|
||||||
|
|
||||||
|
[feature.vc]
|
||||||
|
category = "testcategory"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!debug"
|
||||||
|
feature = "debug"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!welcome"
|
||||||
|
feature = "welcome"
|
||||||
|
|
||||||
|
[[command]]
|
||||||
|
trigger = "!timeout"
|
||||||
|
feature = "timeout"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user