Indent with 2 spaces instead of 4
This commit is contained in:
parent
5c7efcd10e
commit
39083d8248
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@ -0,0 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
@ -5,6 +5,6 @@ import org.javacord.api.DiscordApi
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
object Globals {
|
||||
lateinit var api: DiscordApi
|
||||
val commandCounter: AtomicInteger = AtomicInteger(Dao.getCommandCounter())
|
||||
lateinit var api: DiscordApi
|
||||
val commandCounter: AtomicInteger = AtomicInteger(Dao.getCommandCounter())
|
||||
}
|
||||
|
@ -12,44 +12,44 @@ import java.io.File
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
fun main() {
|
||||
Kagebot.init()
|
||||
Kagebot.init()
|
||||
}
|
||||
|
||||
object Kagebot {
|
||||
fun MessageCreateEvent.process() {
|
||||
if (messageAuthor.isBotUser) {
|
||||
handleOwn()
|
||||
return
|
||||
}
|
||||
Config.commands
|
||||
.find { it.matches(readableMessageContent) && it.isAllowed(this) }
|
||||
.map { it.execute(this) }
|
||||
fun MessageCreateEvent.process() {
|
||||
if (messageAuthor.isBotUser) {
|
||||
handleOwn()
|
||||
return
|
||||
}
|
||||
Config.commands
|
||||
.find { it.matches(readableMessageContent) && it.isAllowed(this) }
|
||||
.map { it.execute(this) }
|
||||
}
|
||||
|
||||
private fun MessageCreateEvent.handleOwn() {
|
||||
if (messageAuthor.isYourself) {
|
||||
val loggedMessage = readableMessageContent.ifBlank { "[embed]" }
|
||||
Log.info("<Self> $loggedMessage")
|
||||
}
|
||||
private fun MessageCreateEvent.handleOwn() {
|
||||
if (messageAuthor.isYourself) {
|
||||
val loggedMessage = readableMessageContent.ifBlank { "[embed]" }
|
||||
Log.info("<Self> $loggedMessage")
|
||||
}
|
||||
}
|
||||
|
||||
fun init() {
|
||||
val secret = File("secret").readText().trim()
|
||||
val api = DiscordApiBuilder().setToken(secret).login().join()
|
||||
Globals.api = api
|
||||
ConfigParser.initialLoad(ConfigParser.DEFAULT_CONFIG_PATH).mapLeft { e ->
|
||||
println("Config parsing error:\n$e,\n${e.message},\n${e.stackTrace.joinToString("\n")}")
|
||||
exitProcess(1)
|
||||
}
|
||||
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()
|
||||
fun init() {
|
||||
val secret = File("secret").readText().trim()
|
||||
val api = DiscordApiBuilder().setToken(secret).login().join()
|
||||
Globals.api = api
|
||||
ConfigParser.initialLoad(ConfigParser.DEFAULT_CONFIG_PATH).mapLeft { e ->
|
||||
println("Config parsing error:\n$e,\n${e.message},\n${e.stackTrace.joinToString("\n")}")
|
||||
exitProcess(1)
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -8,30 +8,30 @@ import java.util.logging.LogRecord
|
||||
import java.util.logging.Logger
|
||||
|
||||
object Log {
|
||||
private val log: Logger by lazy {
|
||||
Logger.getGlobal().apply {
|
||||
addHandler(
|
||||
FileHandler("kagebot.log", true).apply {
|
||||
formatter = LogFormatter()
|
||||
}
|
||||
)
|
||||
private val log: Logger by lazy {
|
||||
Logger.getGlobal().apply {
|
||||
addHandler(
|
||||
FileHandler("kagebot.log", true).apply {
|
||||
formatter = LogFormatter()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun info(message: String) {
|
||||
log.info(message)
|
||||
}
|
||||
fun info(message: String) {
|
||||
log.info(message)
|
||||
}
|
||||
|
||||
fun warn(message: String) {
|
||||
log.warning(message)
|
||||
}
|
||||
fun warn(message: String) {
|
||||
log.warning(message)
|
||||
}
|
||||
}
|
||||
|
||||
private class LogFormatter : Formatter() {
|
||||
private val timeFormatter: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault())
|
||||
private val timeFormatter: DateTimeFormatter =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault())
|
||||
|
||||
override fun format(record: LogRecord): String {
|
||||
return "[${record.level}] ${timeFormatter.format(record.instant)}: ${record.message}\n"
|
||||
}
|
||||
override fun format(record: LogRecord): String {
|
||||
return "[${record.level}] ${timeFormatter.format(record.instant)}: ${record.message}\n"
|
||||
}
|
||||
}
|
||||
|
@ -9,53 +9,53 @@ import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
object MessageUtil {
|
||||
fun MessageAuthor.mention() = "<@$id>"
|
||||
fun MessageAuthor.mention() = "<@$id>"
|
||||
|
||||
fun withEmbed(op: EmbedBuilder.() -> Unit): EmbedBuilder {
|
||||
return EmbedBuilder().apply {
|
||||
Config.server.icon.ifPresent { setThumbnail(it) }
|
||||
setColor(SystemSpec.color)
|
||||
op()
|
||||
}
|
||||
fun withEmbed(op: EmbedBuilder.() -> Unit): EmbedBuilder {
|
||||
return EmbedBuilder().apply {
|
||||
Config.server.icon.ifPresent { setThumbnail(it) }
|
||||
setColor(SystemSpec.color)
|
||||
op()
|
||||
}
|
||||
}
|
||||
|
||||
fun Messageable.sendEmbed(op: EmbedBuilder.() -> Unit) {
|
||||
val embed = withEmbed {
|
||||
setTimestampToNow()
|
||||
op()
|
||||
}
|
||||
sendMessage(embed)
|
||||
fun Messageable.sendEmbed(op: EmbedBuilder.() -> Unit) {
|
||||
val embed = withEmbed {
|
||||
setTimestampToNow()
|
||||
op()
|
||||
}
|
||||
sendMessage(embed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send and embed and add the current time to it.
|
||||
* The time is not set in [withEmbed] because of https://git.kageru.moe/kageru/discord-kagebot/issues/13.
|
||||
*/
|
||||
fun sendEmbed(target: Messageable, embed: EmbedBuilder): CompletableFuture<Message> {
|
||||
return target.sendMessage(embed.setTimestampToNow())
|
||||
}
|
||||
/**
|
||||
* Send and embed and add the current time to it.
|
||||
* The time is not set in [withEmbed] because of https://git.kageru.moe/kageru/discord-kagebot/issues/13.
|
||||
*/
|
||||
fun sendEmbed(target: Messageable, embed: EmbedBuilder): CompletableFuture<Message> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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())
|
||||
}
|
||||
/**
|
||||
* 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,8 +1,11 @@
|
||||
package moe.kageru.kagebot
|
||||
|
||||
import arrow.core.*
|
||||
import arrow.core.Either
|
||||
import arrow.core.ListK
|
||||
import arrow.core.Option
|
||||
import arrow.core.extensions.either.monad.flatMap
|
||||
import arrow.core.extensions.list.foldable.find
|
||||
import arrow.core.firstOrNone
|
||||
import moe.kageru.kagebot.config.Config.server
|
||||
import moe.kageru.kagebot.extensions.*
|
||||
import org.javacord.api.entity.channel.TextChannel
|
||||
@ -16,80 +19,80 @@ import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CompletionException
|
||||
|
||||
object Util {
|
||||
inline fun <T> T.applyIf(condition: Boolean, op: (T) -> T): T {
|
||||
return if (condition) op(this) else this
|
||||
inline fun <T> T.applyIf(condition: Boolean, op: (T) -> T): T {
|
||||
return if (condition) op(this) else this
|
||||
}
|
||||
|
||||
fun hasOneOf(messageAuthor: MessageAuthor, roles: Set<Role>): Boolean {
|
||||
return messageAuthor.asUser().asOption().flatMap { user ->
|
||||
user.roles().find { it in roles }
|
||||
}.nonEmpty()
|
||||
}
|
||||
|
||||
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 hasOneOf(messageAuthor: MessageAuthor, roles: Set<Role>): Boolean {
|
||||
return messageAuthor.asUser().asOption().flatMap { user ->
|
||||
user.roles().find { it in roles }
|
||||
}.nonEmpty()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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" }
|
||||
fun <T> CompletableFuture<T>.asOption(): Option<T> {
|
||||
return try {
|
||||
Option.just(join())
|
||||
} catch (e: CompletionException) {
|
||||
Option.empty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> ListK<T>.getOnly(): Either<Int, T> {
|
||||
return when (size) {
|
||||
1 -> Either.right(first())
|
||||
else -> Either.left(size)
|
||||
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.getCachedUserByDiscriminatedName(idOrName.removePrefix("@")).asOption()
|
||||
.toEither { "User $idOrName not found" }
|
||||
.flatMap { user ->
|
||||
user.openPrivateChannel().asOption().toEither { "Can’t DM user $idOrName" }
|
||||
}
|
||||
else -> server.channelsByName(idOrName).getOnly().mapLeft { "Found $it channels for $idOrName, expected 1" }
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Option.just(join())
|
||||
} 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.getCachedUserByDiscriminatedName(idOrName.removePrefix("@")).asOption()
|
||||
.toEither { "User $idOrName not found" }
|
||||
.flatMap { user ->
|
||||
user.openPrivateChannel().asOption().toEither { "Can’t DM user $idOrName" }
|
||||
}
|
||||
else -> server.channelsByName(idOrName).getOnly().mapLeft { "Found $it channels for $idOrName, expected 1" }
|
||||
}
|
||||
}
|
||||
|
||||
inline fun checked(op: (() -> Unit)) {
|
||||
try {
|
||||
op()
|
||||
} catch (e: Exception) {
|
||||
Log.warn("An uncaught exception occurred.\n$e")
|
||||
Log.warn(e.stackTrace.joinToString("\n"))
|
||||
MessageUtil.sendEmbed(
|
||||
Globals.api.owner.get(),
|
||||
EmbedBuilder()
|
||||
.setColor(Color.RED)
|
||||
.addField("Error", "kagebot has encountered an error")
|
||||
.addField(
|
||||
"$e", """```
|
||||
inline fun checked(op: (() -> Unit)) {
|
||||
try {
|
||||
op()
|
||||
} catch (e: Exception) {
|
||||
Log.warn("An uncaught exception occurred.\n$e")
|
||||
Log.warn(e.stackTrace.joinToString("\n"))
|
||||
MessageUtil.sendEmbed(
|
||||
Globals.api.owner.get(),
|
||||
EmbedBuilder()
|
||||
.setColor(Color.RED)
|
||||
.addField("Error", "kagebot has encountered an error")
|
||||
.addField(
|
||||
"$e", """```
|
||||
${e.stackTrace.joinToString("\n\t")}
|
||||
```""".trimIndent().run { applyIf(length > 1800) { substring(1..1800) } }
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,47 +14,47 @@ import org.javacord.api.event.message.MessageCreateEvent
|
||||
private const val AUTHOR_PLACEHOLDER = "@@"
|
||||
|
||||
class Command(
|
||||
val trigger: String,
|
||||
private val response: String? = null,
|
||||
private val permissions: Permissions?,
|
||||
@JsonProperty("action")
|
||||
private val actions: MessageActions?,
|
||||
embed: List<String>?,
|
||||
feature: String?,
|
||||
matchType: String?
|
||||
val trigger: String,
|
||||
private val response: String? = null,
|
||||
private val permissions: Permissions?,
|
||||
@JsonProperty("action")
|
||||
private val actions: MessageActions?,
|
||||
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) }
|
||||
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) }
|
||||
|
||||
fun matches(msg: String) = this.matchType.matches(msg, this)
|
||||
fun matches(msg: String) = this.matchType.matches(msg, this)
|
||||
|
||||
fun isAllowed(message: MessageCreateEvent) = permissions?.isAllowed(message) ?: true
|
||||
fun isAllowed(message: MessageCreateEvent) = permissions?.isAllowed(message) ?: true
|
||||
|
||||
fun execute(message: MessageCreateEvent) {
|
||||
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.embed?.let {
|
||||
MessageUtil.sendEmbed(message.channel, embed)
|
||||
}
|
||||
this.feature?.handle(message)
|
||||
fun execute(message: MessageCreateEvent) {
|
||||
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.embed?.let {
|
||||
MessageUtil.sendEmbed(message.channel, embed)
|
||||
}
|
||||
this.feature?.handle(message)
|
||||
}
|
||||
|
||||
private fun respond(author: MessageAuthor, response: String) =
|
||||
response.replace(AUTHOR_PLACEHOLDER, author.mention())
|
||||
private fun respond(author: MessageAuthor, response: String) =
|
||||
response.replace(AUTHOR_PLACEHOLDER, author.mention())
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
enum class MatchType(val matches: (String, Command) -> Boolean) {
|
||||
PREFIX({ message, command -> message.startsWith(command.trigger, ignoreCase = true) }),
|
||||
CONTAINS({ message, command -> message.contains(command.trigger, ignoreCase = true) }),
|
||||
REGEX({ message, command -> command.regex!!.matches(message) });
|
||||
PREFIX({ message, command -> message.startsWith(command.trigger, ignoreCase = true) }),
|
||||
CONTAINS({ message, command -> message.contains(command.trigger, ignoreCase = true) }),
|
||||
REGEX({ message, command -> command.regex!!.matches(message) });
|
||||
}
|
||||
|
@ -8,31 +8,31 @@ import moe.kageru.kagebot.config.LocalizationSpec
|
||||
import org.javacord.api.event.message.MessageCreateEvent
|
||||
|
||||
class MessageActions(
|
||||
private val delete: Boolean = false,
|
||||
private val redirect: MessageRedirect?,
|
||||
@JsonProperty("assign")
|
||||
private val assignment: RoleAssignment?
|
||||
private val delete: Boolean = false,
|
||||
private val redirect: MessageRedirect?,
|
||||
@JsonProperty("assign")
|
||||
private val assignment: RoleAssignment?
|
||||
) {
|
||||
|
||||
fun run(message: MessageCreateEvent, command: Command) {
|
||||
if (delete) {
|
||||
deleteMessage(message)
|
||||
}
|
||||
redirect?.execute(message, command)
|
||||
assignment?.assign(message)
|
||||
fun run(message: MessageCreateEvent, command: Command) {
|
||||
if (delete) {
|
||||
deleteMessage(message)
|
||||
}
|
||||
redirect?.execute(message, command)
|
||||
assignment?.assign(message)
|
||||
}
|
||||
|
||||
private fun deleteMessage(message: MessageCreateEvent) {
|
||||
if (message.message.canYouDelete()) {
|
||||
message.deleteMessage()
|
||||
message.messageAuthor.asUser().ifPresent { user ->
|
||||
user.sendEmbed {
|
||||
addField("__Blacklisted__", Config.localization[LocalizationSpec.messageDeleted])
|
||||
addField("Original:", "“${message.readableMessageContent}”")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.info("Tried to delete a message without the necessary permissions. Channel: ${message.channel.id}")
|
||||
private fun deleteMessage(message: MessageCreateEvent) {
|
||||
if (message.message.canYouDelete()) {
|
||||
message.deleteMessage()
|
||||
message.messageAuthor.asUser().ifPresent { user ->
|
||||
user.sendEmbed {
|
||||
addField("__Blacklisted__", Config.localization[LocalizationSpec.messageDeleted])
|
||||
addField("Original:", "“${message.readableMessageContent}”")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.info("Tried to delete a message without the necessary permissions. Channel: ${message.channel.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,29 +12,29 @@ 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()
|
||||
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")
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,22 +7,22 @@ import org.javacord.api.entity.permission.Role
|
||||
import org.javacord.api.event.message.MessageCreateEvent
|
||||
|
||||
class Permissions(
|
||||
hasOneOf: List<String>?,
|
||||
hasNoneOf: List<String>?,
|
||||
private val onlyDM: Boolean = false
|
||||
hasOneOf: List<String>?,
|
||||
hasNoneOf: List<String>?,
|
||||
private val onlyDM: Boolean = false
|
||||
) {
|
||||
private val hasOneOf: Option<Set<Role>> = resolveRoles(hasOneOf)
|
||||
private val hasNoneOf: Option<Set<Role>> = resolveRoles(hasNoneOf)
|
||||
private val hasOneOf: Option<Set<Role>> = resolveRoles(hasOneOf)
|
||||
private val hasNoneOf: Option<Set<Role>> = resolveRoles(hasNoneOf)
|
||||
|
||||
private fun resolveRoles(hasOneOf: List<String>?): Option<Set<Role>> {
|
||||
return Option.fromNullable(hasOneOf?.mapTo(mutableSetOf(), { Util.findRole(it).unwrap() }))
|
||||
}
|
||||
private fun resolveRoles(hasOneOf: List<String>?): Option<Set<Role>> {
|
||||
return Option.fromNullable(hasOneOf?.mapTo(mutableSetOf(), { Util.findRole(it).unwrap() }))
|
||||
}
|
||||
|
||||
fun isAllowed(message: MessageCreateEvent): Boolean = when {
|
||||
message.messageAuthor.isBotOwner -> true
|
||||
onlyDM && !message.isPrivateMessage -> false
|
||||
// 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 = when {
|
||||
message.messageAuthor.isBotOwner -> true
|
||||
onlyDM && !message.isPrivateMessage -> false
|
||||
// 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) }
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,10 @@ 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()
|
||||
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.") }
|
||||
)
|
||||
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.") }
|
||||
)
|
||||
}
|
||||
|
@ -9,18 +9,18 @@ import moe.kageru.kagebot.features.Features
|
||||
import org.javacord.api.entity.server.Server
|
||||
|
||||
object Config {
|
||||
val systemSpec = Config { addSpec(SystemSpec) }.from.toml
|
||||
val localeSpec = Config { addSpec(LocalizationSpec) }.from.toml
|
||||
val commandSpec = Config { addSpec(CommandSpec) }.from.toml
|
||||
val featureSpec = Config { addSpec(FeatureSpec) }.from.toml
|
||||
val systemSpec = Config { addSpec(SystemSpec) }.from.toml
|
||||
val localeSpec = Config { addSpec(LocalizationSpec) }.from.toml
|
||||
val commandSpec = Config { addSpec(CommandSpec) }.from.toml
|
||||
val featureSpec = Config { addSpec(FeatureSpec) }.from.toml
|
||||
|
||||
lateinit var system: Config
|
||||
lateinit var localization: Config
|
||||
lateinit var commandConfig: Config
|
||||
lateinit var featureConfig: Config
|
||||
lateinit var server: Server
|
||||
lateinit var system: Config
|
||||
lateinit var localization: Config
|
||||
lateinit var commandConfig: Config
|
||||
lateinit var featureConfig: Config
|
||||
lateinit var server: Server
|
||||
|
||||
// for easier access
|
||||
val features: Features get() = featureConfig[FeatureSpec.features]
|
||||
val commands: ListK<Command> get() = commandConfig[CommandSpec.command].k()
|
||||
// for easier access
|
||||
val features: Features get() = featureConfig[FeatureSpec.features]
|
||||
val commands: ListK<Command> get() = commandConfig[CommandSpec.command].k()
|
||||
}
|
||||
|
@ -7,28 +7,28 @@ 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)
|
||||
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)
|
||||
}
|
||||
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())
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@ -7,21 +7,21 @@ 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])!! }
|
||||
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.")
|
||||
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>())
|
||||
val command by optional(emptyList<Command>())
|
||||
}
|
||||
|
||||
object FeatureSpec : ConfigSpec(prefix = "") {
|
||||
val features by optional(Features(), name = "feature")
|
||||
val features by optional(Features(), name = "feature")
|
||||
}
|
||||
|
@ -6,16 +6,16 @@ import kotlinx.coroutines.launch
|
||||
import moe.kageru.kagebot.config.Config
|
||||
|
||||
object CronD {
|
||||
fun startAll() {
|
||||
GlobalScope.launch {
|
||||
minutely()
|
||||
}
|
||||
fun startAll() {
|
||||
GlobalScope.launch {
|
||||
minutely()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun minutely() {
|
||||
while (true) {
|
||||
Config.features.timeout?.checkAndRelease()
|
||||
delay(60_000)
|
||||
}
|
||||
private suspend fun minutely() {
|
||||
while (true) {
|
||||
Config.features.timeout?.checkAndRelease()
|
||||
delay(60_000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,16 +7,16 @@ import arrow.core.getOrElse
|
||||
import arrow.typeclasses.Functor
|
||||
|
||||
fun <L, R> Either<L, R>.on(op: (R) -> Unit): Either<L, R> {
|
||||
this.map { op(it) }
|
||||
return this
|
||||
this.map { op(it) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun <T> Either<*, T>.unwrap(): T = getOrElse { 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) } }
|
||||
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) } }
|
||||
AP.run { op(b).map { Tuple3(a, it, c) } }
|
||||
}
|
||||
|
@ -3,10 +3,10 @@ package moe.kageru.kagebot.extensions
|
||||
import arrow.core.ListK
|
||||
import arrow.core.Option
|
||||
import arrow.core.k
|
||||
import org.javacord.api.entity.channel.ServerTextChannel
|
||||
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
|
||||
|
@ -11,51 +11,51 @@ import java.time.temporal.ChronoUnit
|
||||
|
||||
class DebugFeature : MessageFeature {
|
||||
|
||||
override fun handle(message: MessageCreateEvent) {
|
||||
if (message.messageAuthor.isBotOwner) {
|
||||
MessageUtil.sendEmbed(message.channel, getPerformanceStats())
|
||||
}
|
||||
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 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 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 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 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 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"
|
||||
private fun getOsInfo() = "Running on ${System.getProperty("os.name")} " +
|
||||
"${System.getProperty("os.version")}-${System.getProperty("os.arch")}.\n"
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ import org.javacord.api.DiscordApi
|
||||
import org.javacord.api.event.message.MessageCreateEvent
|
||||
|
||||
interface MessageFeature {
|
||||
fun handle(message: MessageCreateEvent)
|
||||
fun handle(message: MessageCreateEvent)
|
||||
}
|
||||
|
||||
interface EventFeature {
|
||||
fun register(api: DiscordApi)
|
||||
fun register(api: DiscordApi)
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
package moe.kageru.kagebot.features
|
||||
|
||||
class Features(
|
||||
val welcome: WelcomeFeature? = null,
|
||||
val timeout: TimeoutFeature? = null,
|
||||
vc: TempVCFeature = TempVCFeature(null)
|
||||
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()
|
||||
private val debug = DebugFeature()
|
||||
private val help = HelpFeature()
|
||||
private val getConfig = GetConfigFeature()
|
||||
private val setConfig = SetConfigFeature()
|
||||
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
fun findByString(feature: String) = featureMap[feature]
|
||||
fun eventFeatures() = all.filterIsInstance<EventFeature>()
|
||||
fun findByString(feature: String) = featureMap[feature]
|
||||
fun eventFeatures() = all.filterIsInstance<EventFeature>()
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ 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)
|
||||
}
|
||||
override fun handle(message: MessageCreateEvent) {
|
||||
message.channel.sendMessage(ConfigParser.configFile)
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,13 @@ 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))
|
||||
}
|
||||
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 }
|
||||
.filter { it.matchType == MatchType.PREFIX && it.isAllowed(message) }
|
||||
.joinToString("\n") { it.trigger }
|
||||
|
@ -6,23 +6,23 @@ 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}```")
|
||||
}
|
||||
}
|
||||
@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}```")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,46 +16,46 @@ 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() }
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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"
|
||||
private fun generateChannelName(message: MessageCreateEvent): String =
|
||||
"${message.messageAuthor.name}’s volatile corner"
|
||||
}
|
||||
|
@ -23,58 +23,58 @@ import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
class TimeoutFeature(@JsonProperty("role") role: String) : MessageFeature {
|
||||
private val timeoutRole: Role = findRole(role).unwrap()
|
||||
private val timeoutRole: Role = findRole(role).unwrap()
|
||||
|
||||
override fun handle(message: MessageCreateEvent) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
)
|
||||
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}")
|
||||
}
|
||||
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()}")
|
||||
}
|
||||
)
|
||||
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()}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -13,41 +13,41 @@ import org.javacord.api.event.message.MessageCreateEvent
|
||||
import org.javacord.api.event.server.member.ServerMemberJoinEvent
|
||||
|
||||
class WelcomeFeature(
|
||||
content: List<String>?,
|
||||
fallbackChannel: String?,
|
||||
private val fallbackMessage: String?
|
||||
content: List<String>?,
|
||||
fallbackChannel: String?,
|
||||
private val fallbackMessage: String?
|
||||
) : MessageFeature, EventFeature {
|
||||
val embed: EmbedBuilder? by lazy { content?.let(MessageUtil::listToEmbed) }
|
||||
val embed: EmbedBuilder? by lazy { content?.let(MessageUtil::listToEmbed) }
|
||||
|
||||
override fun register(api: DiscordApi) {
|
||||
api.addServerMemberJoinListener { event ->
|
||||
checked { welcomeUser(event) }
|
||||
}
|
||||
override fun register(api: DiscordApi) {
|
||||
api.addServerMemberJoinListener { event ->
|
||||
checked { welcomeUser(event) }
|
||||
}
|
||||
}
|
||||
|
||||
fun welcomeUser(event: ServerMemberJoinEvent) {
|
||||
Log.info("User ${event.user.discriminatedName} joined")
|
||||
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)
|
||||
)
|
||||
}
|
||||
fun welcomeUser(event: ServerMemberJoinEvent) {
|
||||
Log.info("User ${event.user.discriminatedName} joined")
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -5,40 +5,40 @@ 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()
|
||||
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 saveTimeout(releaseTime: Long, user: Long, roles: List<Long>) {
|
||||
prisoners[releaseTime] = (listOf(user) + roles).toLongArray()
|
||||
}
|
||||
|
||||
fun setCommandCounter(count: Int) {
|
||||
commands["total"] = count
|
||||
}
|
||||
fun setCommandCounter(count: Int) {
|
||||
commands["total"] = count
|
||||
}
|
||||
|
||||
fun getCommandCounter() = commands["total"] ?: 0
|
||||
fun getCommandCounter() = commands["total"] ?: 0
|
||||
|
||||
fun close() = db.close()
|
||||
fun close() = db.close()
|
||||
|
||||
fun getAllTimeouts() = prisoners.keys.k()
|
||||
fun getAllTimeouts() = prisoners.keys.k()
|
||||
|
||||
fun deleteTimeout(releaseTime: Long): List<Long> {
|
||||
val timeout = prisoners[releaseTime]!!
|
||||
prisoners.remove(releaseTime)
|
||||
return timeout.toList()
|
||||
}
|
||||
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 isTemporaryVC(channel: String): Boolean {
|
||||
return channel in tempVcs
|
||||
}
|
||||
|
||||
fun addTemporaryVC(channel: String) {
|
||||
tempVcs.add(channel)
|
||||
}
|
||||
fun addTemporaryVC(channel: String) {
|
||||
tempVcs.add(channel)
|
||||
}
|
||||
|
||||
fun removeTemporaryVC(channel: String) {
|
||||
tempVcs.remove(channel)
|
||||
}
|
||||
fun removeTemporaryVC(channel: String) {
|
||||
tempVcs.remove(channel)
|
||||
}
|
||||
}
|
||||
|
@ -13,27 +13,27 @@ import java.awt.Color
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
class ConfigTest : ShouldSpec({
|
||||
TestUtil.prepareTestEnvironment()
|
||||
"should properly parse test config" {
|
||||
Config.system[SystemSpec.serverId] shouldNotBe null
|
||||
SystemSpec.color shouldBe Color.decode("#1793d0")
|
||||
Config.features.welcome!!.embed shouldNotBe null
|
||||
Config.commands.size shouldBe 3
|
||||
}
|
||||
TestUtil.prepareTestEnvironment()
|
||||
"should properly parse test config" {
|
||||
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
|
||||
}
|
||||
"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
|
||||
}
|
||||
})
|
||||
|
@ -23,124 +23,124 @@ import java.io.File
|
||||
import java.util.*
|
||||
|
||||
object TestUtil {
|
||||
private val TIMEOUT_ROLE = mockk<Role> {
|
||||
every { id } returns 123
|
||||
}
|
||||
val TEST_ROLE = mockk<Role> {
|
||||
every { id } returns 1
|
||||
every { isManaged } returns false
|
||||
}
|
||||
private val TIMEOUT_ROLE = mockk<Role> {
|
||||
every { id } returns 123
|
||||
}
|
||||
val TEST_ROLE = mockk<Role> {
|
||||
every { id } returns 1
|
||||
every { isManaged } returns false
|
||||
}
|
||||
|
||||
fun mockMessage(
|
||||
content: String,
|
||||
replies: MutableList<String> = mutableListOf(),
|
||||
replyEmbeds: MutableList<EmbedBuilder> = mutableListOf(),
|
||||
files: MutableList<File> = mutableListOf(),
|
||||
isBot: Boolean = false
|
||||
): MessageCreateEvent {
|
||||
return mockk {
|
||||
every { messageContent } returns content
|
||||
every { readableMessageContent } returns content
|
||||
every { channel.sendMessage(capture(replies)) } returns mockk(relaxed = true) {
|
||||
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 mockMessage(
|
||||
content: String,
|
||||
replies: MutableList<String> = mutableListOf(),
|
||||
replyEmbeds: MutableList<EmbedBuilder> = mutableListOf(),
|
||||
files: MutableList<File> = mutableListOf(),
|
||||
isBot: Boolean = false
|
||||
): MessageCreateEvent {
|
||||
return mockk {
|
||||
every { messageContent } returns content
|
||||
every { readableMessageContent } returns content
|
||||
every { channel.sendMessage(capture(replies)) } returns mockk(relaxed = true) {
|
||||
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)
|
||||
}
|
||||
|
||||
fun messageableAuthor(messages: MutableList<EmbedBuilder> = mutableListOf()): User {
|
||||
return mockk {
|
||||
every { roles() } returns ListK.empty()
|
||||
every { sendMessage(capture(messages)) } returns mockk(relaxed = true)
|
||||
Globals.api = mockk(relaxed = true) {
|
||||
every { getServerById(any<String>()) } returns Optional.of(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 { membersByName(any()) } returns ListK.just(mockk(relaxed = true) {
|
||||
every { id } returns 123
|
||||
every { roles() } returns ListK.just(TEST_ROLE)
|
||||
every { sendMessage(capture(dmEmbeds)) } returns mockk(relaxed = true) {
|
||||
every { isCompletedExceptionally } returns false
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Config.server = Globals.api.getServerById("").get()
|
||||
ConfigParser.initialLoad("testconfig.toml")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Globals.api = mockk(relaxed = true) {
|
||||
every { getServerById(any<String>()) } returns Optional.of(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 { membersByName(any()) } returns ListK.just(mockk(relaxed = true) {
|
||||
every { id } returns 123
|
||||
every { roles() } returns ListK.just(TEST_ROLE)
|
||||
every { sendMessage(capture(dmEmbeds)) } returns mockk(relaxed = true) {
|
||||
every { isCompletedExceptionally } returns false
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Config.server = Globals.api.getServerById("").get()
|
||||
ConfigParser.initialLoad("testconfig.toml")
|
||||
}
|
||||
fun testMessageSuccess(content: String, result: String) {
|
||||
val calls = mutableListOf<String>()
|
||||
mockMessage(content, replies = calls).process()
|
||||
calls shouldBe mutableListOf(result)
|
||||
}
|
||||
|
||||
fun testMessageSuccess(content: String, result: String) {
|
||||
val calls = mutableListOf<String>()
|
||||
mockMessage(content, replies = calls).process()
|
||||
calls shouldBe mutableListOf(result)
|
||||
}
|
||||
fun embedToString(embed: EmbedBuilder): String {
|
||||
return (embed.delegate as EmbedBuilderDelegateImpl).toJsonNode().toString()
|
||||
}
|
||||
|
||||
fun embedToString(embed: EmbedBuilder): String {
|
||||
return (embed.delegate as EmbedBuilderDelegateImpl).toJsonNode().toString()
|
||||
}
|
||||
fun withCommands(config: String, test: (() -> Unit)) {
|
||||
val oldCmds = Config.commandConfig
|
||||
Config.commandConfig = Config.commandSpec.string(config)
|
||||
test()
|
||||
Config.commandConfig = oldCmds
|
||||
}
|
||||
|
||||
fun withCommands(config: String, test: (() -> Unit)) {
|
||||
val oldCmds = Config.commandConfig
|
||||
Config.commandConfig = Config.commandSpec.string(config)
|
||||
test()
|
||||
Config.commandConfig = oldCmds
|
||||
fun withReplyContents(
|
||||
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
|
||||
}
|
||||
|
||||
fun withReplyContents(
|
||||
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) {
|
||||
replyString shouldNotContain string
|
||||
}
|
||||
for (string in unexpected) {
|
||||
replyString shouldNotContain string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,284 +27,284 @@ import org.javacord.api.entity.user.User
|
||||
import java.util.*
|
||||
|
||||
class CommandTest : StringSpec({
|
||||
prepareTestEnvironment()
|
||||
"should increment command counter" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!ping"
|
||||
response = "pong"
|
||||
""".trimIndent()
|
||||
) {
|
||||
val before = Globals.commandCounter.get()
|
||||
testMessageSuccess("!ping", "pong")
|
||||
Globals.commandCounter.get() shouldBe (before + 1)
|
||||
}
|
||||
prepareTestEnvironment()
|
||||
"should increment command counter" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!ping"
|
||||
response = "pong"
|
||||
""".trimIndent()
|
||||
) {
|
||||
val before = Globals.commandCounter.get()
|
||||
testMessageSuccess("!ping", "pong")
|
||||
Globals.commandCounter.get() shouldBe (before + 1)
|
||||
}
|
||||
"should match prefix command" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!ping"
|
||||
response = "pong"
|
||||
""".trimIndent()
|
||||
) {
|
||||
testMessageSuccess("!ping", "pong")
|
||||
}
|
||||
}
|
||||
"should match prefix command" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!ping"
|
||||
response = "pong"
|
||||
""".trimIndent()
|
||||
) {
|
||||
testMessageSuccess("!ping", "pong")
|
||||
}
|
||||
"should print embed for command" {
|
||||
val calls = mutableListOf<EmbedBuilder>()
|
||||
prepareTestEnvironment(calls)
|
||||
val heading = "heading 1"
|
||||
val content = "this is the first paragraph of the embed"
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!embed"
|
||||
embed = [ "$heading", "$content" ]
|
||||
""".trimIndent()
|
||||
) {
|
||||
TestUtil.withReplyContents(expected = listOf(heading, content)) {
|
||||
mockMessage("!embed", replyEmbeds = it).process()
|
||||
}
|
||||
}
|
||||
}
|
||||
"should print embed for command" {
|
||||
val calls = mutableListOf<EmbedBuilder>()
|
||||
prepareTestEnvironment(calls)
|
||||
val heading = "heading 1"
|
||||
val content = "this is the first paragraph of the embed"
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!embed"
|
||||
embed = [ "$heading", "$content" ]
|
||||
""".trimIndent()
|
||||
) {
|
||||
TestUtil.withReplyContents(expected = listOf(heading, content)) {
|
||||
mockMessage("!embed", replyEmbeds = it).process()
|
||||
}
|
||||
}
|
||||
"should match contains command" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "somewhere"
|
||||
response = "found it"
|
||||
matchType = "CONTAINS"
|
||||
""".trimIndent()
|
||||
) {
|
||||
testMessageSuccess("the trigger is somewhere in this message", "found it")
|
||||
}
|
||||
}
|
||||
"should match contains command" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "somewhere"
|
||||
response = "found it"
|
||||
matchType = "CONTAINS"
|
||||
""".trimIndent()
|
||||
) {
|
||||
testMessageSuccess("the trigger is somewhere in this message", "found it")
|
||||
}
|
||||
"should match regex command" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "A.+B"
|
||||
response = "regex matched"
|
||||
matchType = "REGEX"
|
||||
""".trimIndent()
|
||||
) {
|
||||
testMessageSuccess("AcsdB", "regex matched")
|
||||
}
|
||||
}
|
||||
"should match regex command" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "A.+B"
|
||||
response = "regex matched"
|
||||
matchType = "REGEX"
|
||||
""".trimIndent()
|
||||
) {
|
||||
testMessageSuccess("AcsdB", "regex matched")
|
||||
}
|
||||
"should ping author" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "answer me"
|
||||
response = "@@ there you go"
|
||||
""".trimIndent()
|
||||
) {
|
||||
testMessageSuccess("answer me", "<@1> there you go")
|
||||
}
|
||||
}
|
||||
"should ping author" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "answer me"
|
||||
response = "@@ there you go"
|
||||
""".trimIndent()
|
||||
) {
|
||||
testMessageSuccess("answer me", "<@1> there you go")
|
||||
}
|
||||
"should not react to own message" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!ping"
|
||||
response = "pong"
|
||||
""".trimIndent()
|
||||
) {
|
||||
val calls = mutableListOf<String>()
|
||||
mockMessage("!ping", replies = calls, isBot = true).process()
|
||||
calls shouldBe mutableListOf()
|
||||
}
|
||||
}
|
||||
"should not react to own message" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!ping"
|
||||
response = "pong"
|
||||
""".trimIndent()
|
||||
) {
|
||||
val calls = mutableListOf<String>()
|
||||
mockMessage("!ping", replies = calls, isBot = true).process()
|
||||
calls shouldBe mutableListOf()
|
||||
}
|
||||
"should delete messages and send copy to author" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "delet this"
|
||||
[command.action]
|
||||
delete = true
|
||||
""".trimIndent()
|
||||
) {
|
||||
val messageContent = "delet this"
|
||||
TestUtil.withReplyContents(expected = listOf(messageContent)) {
|
||||
val mockMessage = mockMessage(messageContent)
|
||||
every { mockMessage.deleteMessage() } returns mockk()
|
||||
every { mockMessage.messageAuthor.asUser() } returns Optional.of(messageableAuthor(it))
|
||||
mockMessage.process()
|
||||
}
|
||||
}
|
||||
}
|
||||
"should delete messages and send copy to author" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "delet this"
|
||||
[command.action]
|
||||
delete = true
|
||||
""".trimIndent()
|
||||
) {
|
||||
val messageContent = "delet this"
|
||||
TestUtil.withReplyContents(expected = listOf(messageContent)) {
|
||||
val mockMessage = mockMessage(messageContent)
|
||||
every { mockMessage.deleteMessage() } returns mockk()
|
||||
every { mockMessage.messageAuthor.asUser() } returns Optional.of(messageableAuthor(it))
|
||||
mockMessage.process()
|
||||
}
|
||||
}
|
||||
"should refuse command without permissions" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!restricted"
|
||||
response = "access granted"
|
||||
[command.permissions]
|
||||
hasOneOf = [
|
||||
"testrole",
|
||||
]
|
||||
""".trimIndent()
|
||||
) {
|
||||
val replies = mutableListOf<String>()
|
||||
val mockMessage = mockMessage("!restricted", replies = replies)
|
||||
mockMessage.process()
|
||||
replies shouldBe mutableListOf()
|
||||
}
|
||||
}
|
||||
"should refuse command without permissions" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!restricted"
|
||||
response = "access granted"
|
||||
[command.permissions]
|
||||
hasOneOf = [
|
||||
"testrole",
|
||||
]
|
||||
""".trimIndent()
|
||||
) {
|
||||
val replies = mutableListOf<String>()
|
||||
val mockMessage = mockMessage("!restricted", replies = replies)
|
||||
mockMessage.process()
|
||||
replies shouldBe mutableListOf()
|
||||
}
|
||||
"should accept restricted command for owner" {
|
||||
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.isBotOwner } returns true
|
||||
mockMessage.process()
|
||||
calls shouldBe mutableListOf("access granted")
|
||||
}
|
||||
}
|
||||
"should accept restricted command for owner" {
|
||||
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.isBotOwner } returns true
|
||||
mockMessage.process()
|
||||
calls shouldBe mutableListOf("access granted")
|
||||
}
|
||||
"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 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()
|
||||
}
|
||||
"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
|
||||
every { mockMessage.messageAuthor.asUser() } returns mockk {
|
||||
every { isPresent } returns true
|
||||
every { get().getRoles(any()) } returns emptyList()
|
||||
}
|
||||
mockMessage.process()
|
||||
// first message didn’t answer anything
|
||||
calls shouldBe mutableListOf("access granted")
|
||||
}
|
||||
// without the role
|
||||
every { mockMessage.messageAuthor.asUser() } returns mockk {
|
||||
every { isPresent } returns true
|
||||
every { get().getRoles(any()) } returns emptyList()
|
||||
}
|
||||
mockMessage.process()
|
||||
// first message didn’t answer anything
|
||||
calls shouldBe mutableListOf("access granted")
|
||||
}
|
||||
"should refuse DM only message in server channel" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!dm"
|
||||
response = "access granted"
|
||||
[command.permissions]
|
||||
onlyDM = true
|
||||
""".trimIndent()
|
||||
) {
|
||||
val calls = mutableListOf<String>()
|
||||
mockMessage("!dm", replies = calls).process()
|
||||
calls shouldBe mutableListOf()
|
||||
}
|
||||
}
|
||||
"should refuse DM only message in server channel" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!dm"
|
||||
response = "access granted"
|
||||
[command.permissions]
|
||||
onlyDM = true
|
||||
""".trimIndent()
|
||||
) {
|
||||
val calls = mutableListOf<String>()
|
||||
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.
|
||||
*/
|
||||
"should redirect" {
|
||||
val calls = mutableListOf<EmbedBuilder>()
|
||||
prepareTestEnvironment(calls)
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!redirect"
|
||||
response = "redirected"
|
||||
[command.action.redirect]
|
||||
target = "testchannel"
|
||||
anonymous = true
|
||||
""".trimIndent()
|
||||
) {
|
||||
val message = "this is a message"
|
||||
mockMessage("!redirect $message").process()
|
||||
calls.size shouldBe 1
|
||||
embedToString(calls[0]) shouldContain "\"$message\""
|
||||
}
|
||||
}
|
||||
/*
|
||||
* 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>()
|
||||
prepareTestEnvironment(calls)
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!redirect"
|
||||
response = "redirected"
|
||||
[command.action.redirect]
|
||||
target = "testchannel"
|
||||
anonymous = true
|
||||
""".trimIndent()
|
||||
) {
|
||||
val message = "this is a message"
|
||||
mockMessage("!redirect $message").process()
|
||||
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 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 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, got asd")
|
||||
Dao.isTemporaryVC("12345") shouldBe false
|
||||
}
|
||||
}
|
||||
"should reject invalid vc command" {
|
||||
withCommands(
|
||||
"""
|
||||
[[command]]
|
||||
trigger = "!vc"
|
||||
feature = "vc"
|
||||
""".trimIndent()
|
||||
) {
|
||||
testMessageSuccess("!vc asd", "Invalid syntax, expected a number, got asd")
|
||||
Dao.isTemporaryVC("12345") shouldBe false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -9,16 +9,17 @@ 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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -10,23 +10,23 @@ import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||
import org.javacord.api.event.message.MessageCreateEvent
|
||||
|
||||
class DebugFeatureTest : StringSpec({
|
||||
TestUtil.prepareTestEnvironment()
|
||||
// this will fail if the bot tries to execute more than it should
|
||||
// because the mock does not provide the necessary methods
|
||||
"should ignore regular users" {
|
||||
val message = TestUtil.mockMessage("!debug")
|
||||
every { message.messageAuthor.isBotOwner } returns false
|
||||
message.process()
|
||||
DebugFeature().handle(message)
|
||||
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()
|
||||
}
|
||||
DebugFeature().handle(message)
|
||||
verify(exactly = 1) { message.channel.sendMessage(any<EmbedBuilder>()) }
|
||||
TestUtil.prepareTestEnvironment()
|
||||
// this will fail if the bot tries to execute more than it should
|
||||
// because the mock does not provide the necessary methods
|
||||
"should ignore regular users" {
|
||||
val message = TestUtil.mockMessage("!debug")
|
||||
every { message.messageAuthor.isBotOwner } returns false
|
||||
message.process()
|
||||
DebugFeature().handle(message)
|
||||
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()
|
||||
}
|
||||
DebugFeature().handle(message)
|
||||
verify(exactly = 1) { message.channel.sendMessage(any<EmbedBuilder>()) }
|
||||
}
|
||||
})
|
||||
|
@ -14,46 +14,46 @@ 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()
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
"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()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -10,53 +10,53 @@ 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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
private fun clearTimeouts() {
|
||||
Dao.getAllTimeouts().forEach { to ->
|
||||
Dao.deleteTimeout(to)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,37 +10,37 @@ import org.javacord.api.entity.message.embed.EmbedBuilder
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
class WelcomeFeatureTest : StringSpec({
|
||||
TestUtil.prepareTestEnvironment()
|
||||
"should send welcome" {
|
||||
val sentMessages = mutableListOf<EmbedBuilder>()
|
||||
Config.features.welcome!!.welcomeUser(
|
||||
mockk {
|
||||
every { user } returns mockk {
|
||||
every { discriminatedName } returns "testuser#1234"
|
||||
every { sendMessage(capture(sentMessages)) } returns mockk {
|
||||
every { join() } returns mockk()
|
||||
every { isCompletedExceptionally } returns false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
sentMessages shouldBe mutableListOf(Config.features.welcome!!.embed)
|
||||
}
|
||||
"should send welcome fallback if DMs are disabled" {
|
||||
val message = mutableListOf<String>()
|
||||
TestUtil.prepareTestEnvironment(sentMessages = message)
|
||||
Config.features.welcome!!.welcomeUser(
|
||||
mockk {
|
||||
every { user } returns mockk {
|
||||
every { discriminatedName } returns "testuser#1234"
|
||||
every { id } returns 123
|
||||
every { sendMessage(any<EmbedBuilder>()) } returns mockk {
|
||||
every { join() } returns mockk()
|
||||
every { isCompletedExceptionally } returns true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
message shouldBe mutableListOf("<@123> welcome")
|
||||
}
|
||||
TestUtil.prepareTestEnvironment()
|
||||
"should send welcome" {
|
||||
val sentMessages = mutableListOf<EmbedBuilder>()
|
||||
Config.features.welcome!!.welcomeUser(
|
||||
mockk {
|
||||
every { user } returns mockk {
|
||||
every { discriminatedName } returns "testuser#1234"
|
||||
every { sendMessage(capture(sentMessages)) } returns mockk {
|
||||
every { join() } returns mockk()
|
||||
every { isCompletedExceptionally } returns false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
sentMessages shouldBe mutableListOf(Config.features.welcome!!.embed)
|
||||
}
|
||||
"should send welcome fallback if DMs are disabled" {
|
||||
val message = mutableListOf<String>()
|
||||
TestUtil.prepareTestEnvironment(sentMessages = message)
|
||||
Config.features.welcome!!.welcomeUser(
|
||||
mockk {
|
||||
every { user } returns mockk {
|
||||
every { discriminatedName } returns "testuser#1234"
|
||||
every { id } returns 123
|
||||
every { sendMessage(any<EmbedBuilder>()) } returns mockk {
|
||||
every { join() } returns mockk()
|
||||
every { isCompletedExceptionally } returns true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
message shouldBe mutableListOf("<@123> welcome")
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user