Added redirect action

This commit is contained in:
kageru 2019-06-09 18:41:51 +02:00
parent 6d7cd5b776
commit 92dc0a3322
Signed by: kageru
GPG Key ID: 8282A2BEA4ADA3D2
10 changed files with 129 additions and 30 deletions

View File

@ -31,6 +31,9 @@ dependencies {
testImplementation("io.kotlintest:kotlintest-runner-junit5:3.3.2")
testImplementation("io.mockk:mockk:1.9")
// these two are needed to access javacord internals (such as reading from sent embeds during tests)
testImplementation("org.javacord:javacord-core:3.0.4")
testCompile("com.fasterxml.jackson.core:jackson-databind:2.9.9")
}
tasks.withType<KotlinCompile> {

View File

@ -2,6 +2,7 @@ package moe.kageru.kagebot
import moe.kageru.kagebot.Config.Companion.config
import moe.kageru.kagebot.Util.doIf
import moe.kageru.kagebot.Util.ifNotEmpty
import org.javacord.api.entity.message.MessageAuthor
import org.javacord.api.event.message.MessageCreateEvent
@ -11,20 +12,20 @@ class Command(
trigger: String?,
private val response: String?,
matchType: MatchType?,
private val deleteMessage: Boolean,
neededPermissions: Iterable<Long>?
neededPermissions: Iterable<Long>?,
private val actions: MessageActions?
) {
val trigger: String = trigger!!
val regex: Regex? = if (matchType == MatchType.REGEX) Regex(trigger!!) else null
private val matchType: MatchType = matchType ?: MatchType.PREFIX
val matchType: MatchType = matchType ?: MatchType.PREFIX
private val neededRoles = neededPermissions?.toSet()
constructor(cmd: Command) : this(
cmd.trigger,
cmd.response,
cmd.matchType,
cmd.deleteMessage,
cmd.neededRoles
cmd.neededRoles,
cmd.actions
)
fun execute(message: MessageCreateEvent) {
@ -34,20 +35,16 @@ class Command(
return
}
}
if (this.deleteMessage && message.message.canYouDelete()) {
message.deleteMessage()
}
this.actions?.run(message, this)
this.response?.let {
message.channel.sendMessage(respond(message.messageAuthor))
}
}
private fun hasOneOf(messageAuthor: MessageAuthor, roles: Set<Long>): Boolean {
val optional = messageAuthor.asUser()
return when {
optional.isEmpty -> false
else -> optional.get().getRoles(Config.server).map { it.id }.toSet().intersect(roles).isNotEmpty()
}
return messageAuthor.asUser().ifNotEmpty { user ->
user.getRoles(Config.server).map { it.id }.toSet().intersect(roles).isNotEmpty()
} ?: false
}
fun matches(msg: String) = this.matchType.matches(msg, this)
@ -60,9 +57,6 @@ enum class MatchType {
PREFIX {
override fun matches(message: String, command: Command) = message.startsWith(command.trigger)
},
FULL {
override fun matches(message: String, command: Command) = message == command.trigger
},
CONTAINS {
override fun matches(message: String, command: Command) = message.contains(command.trigger)
},

View File

@ -1,6 +1,7 @@
package moe.kageru.kagebot
import com.moandjiezana.toml.Toml
import org.javacord.api.DiscordApi
import org.javacord.api.entity.server.Server
import java.io.File
@ -10,6 +11,8 @@ class Config(val system: System, val localization: Localization, val commands: L
val secret = File("secret").readText().replace("\n", "")
var server: Server? = null
get() = field!!
var api: DiscordApi? = null
get() = field!!
private fun read(path: String): Config {
val rawConfig: Toml = Toml().read(run {
@ -31,5 +34,5 @@ class Config(val system: System, val localization: Localization, val commands: L
}
}
data class System(val serverId: String)
data class Localization(val permissionDenied: String)
data class System(val serverId: String, val color: String)
data class Localization(val permissionDenied: String, val redirectedMessage: String)

View File

@ -23,6 +23,7 @@ class Kagebot {
init {
val api = DiscordApiBuilder().setToken(Config.secret).login().join()
Config.server = api.getServerById(config.system.serverId).orElseThrow()
Config.api = api
Runtime.getRuntime().addShutdownHook(Thread {
log.info("Bot has been interrupted. Shutting down.")
api.disconnect()

View File

@ -0,0 +1,42 @@
package moe.kageru.kagebot
import moe.kageru.kagebot.Config.Companion.config
import moe.kageru.kagebot.Log.log
import moe.kageru.kagebot.Util.ifNotEmpty
import org.javacord.api.event.message.MessageCreateEvent
class MessageActions(private val delete: Boolean, private val redirect: Redirect?) {
fun run(message: MessageCreateEvent, command: Command) {
if (delete && message.message.canYouDelete()) {
message.deleteMessage()
}
redirect?.execute(message, command)
}
}
class Redirect(private val target: Long, private val anonymous: Boolean) {
fun execute(message: MessageCreateEvent, command: Command) {
val embed = MessageUtil.getEmbedBuilder()
.addField(
config.localization.redirectedMessage,
message.readableMessageContent.let { content ->
when (command.matchType) {
MatchType.PREFIX -> content.removePrefix("${command.trigger} ")
else -> content
}
}
)
// No inlined if/else because the types are different.
// Passing the full message author will also include the avatar in the embed.
embed.apply {
if (anonymous) {
setAuthor("Anonymous")
} else {
setAuthor(message.messageAuthor)
}
}
Config.server!!.getTextChannelById(target).ifNotEmpty { it.sendMessage(embed) }
?: log.warning("Could not redirect message to channel $target")
}
}

View File

@ -1,9 +1,22 @@
package moe.kageru.kagebot
import org.javacord.api.entity.message.MessageAuthor
import org.javacord.api.entity.message.embed.EmbedBuilder
import org.javacord.api.event.message.MessageCreateEvent
import java.awt.Color
import moe.kageru.kagebot.Config.Companion.config
object MessageUtil {
fun mention(user: MessageAuthor): String {
return "<@${user.id}>"
}
fun MessageCreateEvent.asString(): String =
"<${this.messageAuthor.discriminatedName}> ${this.readableMessageContent}"
fun getEmbedBuilder(): EmbedBuilder {
val builder = EmbedBuilder()
Config.server!!.icon.ifPresent { builder.setThumbnail(it) }
return builder.setColor(Color.decode(config.system.color)).setTimestampToNow()
}
}

View File

@ -1,13 +1,16 @@
package moe.kageru.kagebot
import org.javacord.api.event.message.MessageCreateEvent
import java.util.*
object Util {
inline fun <T> T.doIf(condition: (T) -> Boolean, op: (T) -> T): T {
return if (condition(this)) op(this) else this
}
fun MessageCreateEvent.asString(): String =
"<${this.messageAuthor.discriminatedName}> ${this.readableMessageContent}"
inline fun <T, R> Optional<T>.ifNotEmpty(op: (T) -> R): R? {
if (this.isPresent) {
return op(this.get())
}
return null
}
}

View File

@ -1,8 +1,11 @@
[system]
serverId = "356414885292277771"
color = "#1793d0"
[localization]
permissionDenied = "You do not have permission to use this command."
# results in <name> says <message>
redirectedMessage = "says"
[[commands]]
trigger = "!ping"
@ -26,7 +29,7 @@ response = "@@ there you go"
[[commands]]
trigger = "delet this"
deleteMessage = true
actions = { delete = true }
[[commands]]
trigger = "!restricted"
@ -37,8 +40,14 @@ neededRoles = [
446668543816106004
]
# redirect every message that starts with !redirect to channel 555097559023222825
[[commands]]
trigger = "^[^`]*([()|DoOvVcC][-=^']?;|;[-=^']?[()|DoOpPvVcC3]|:wink:|😉)[^`]*$"
response = "@@ Oboe!"
matchType = "REGEX"
deleteMessage = true
trigger = "!redirect"
response = "redirected"
actions = { redirect = { target = 555097559023222825 } }
# the same, but without the original username
[[commands]]
trigger = "!anonRedirect"
response = "redirected"
actions = { redirect = {target = 555097559023222825, anonymous = true } }

View File

@ -1,17 +1,20 @@
package moe.kageru.kagebot
import io.kotlintest.matchers.string.shouldContain
import io.kotlintest.shouldBe
import io.kotlintest.specs.StringSpec
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import moe.kageru.kagebot.Config.Companion.config
import org.javacord.api.entity.message.embed.EmbedBuilder
import org.javacord.api.entity.permission.Role
import org.javacord.api.entity.user.User
import org.javacord.core.entity.message.embed.EmbedBuilderDelegateImpl
import java.util.*
class CommandTest : StringSpec({
Config.server = mockk()
TestUtil.prepareServerConfig()
"should match prefix command" {
testMessageSuccess("!ping", "pong")
}
@ -38,8 +41,8 @@ class CommandTest : StringSpec({
"should refuse command without permissions" {
val calls = mutableListOf<String>()
val mockOptional = mockk<Optional<User>>()
every { mockOptional.isEmpty } returns false
every { mockOptional.get().getRoles(any()) } returns emptyList()
every { mockOptional.isPresent } returns true
val mockMessage = TestUtil.mockMessage("!restricted", capturedCalls = calls)
every { mockMessage.messageAuthor.asUser() } returns mockOptional
Kagebot.processMessage(mockMessage)
@ -59,7 +62,7 @@ class CommandTest : StringSpec({
val mockRole = mockk<Role>()
every { mockRole.id } returns 452034011393425409
val mockOptional = mockk<Optional<User>>()
every { mockOptional.isEmpty } returns false
every { mockOptional.isPresent } returns true
every { mockOptional.get().getRoles(any()) } returns listOf(mockRole)
val mockMessage = TestUtil.mockMessage("!restricted", capturedCalls = calls)
every { mockMessage.messageAuthor.asUser() } returns mockOptional
@ -67,6 +70,15 @@ class CommandTest : StringSpec({
calls.size shouldBe 1
calls[0] shouldBe "access granted"
}
"should redirect" {
val calls = mutableListOf<EmbedBuilder>()
TestUtil.prepareServerConfig(calls)
val message = "this is a message"
Kagebot.processMessage(TestUtil.mockMessage("!anonRedirect $message"))
calls.size shouldBe 1
val delegateImpl = calls[0].delegate as EmbedBuilderDelegateImpl
delegateImpl.toJsonNode().toString() shouldContain "\"$message\""
}
}) {
companion object {
fun testMessageSuccess(content: String, result: String) {

View File

@ -1,8 +1,14 @@
package moe.kageru.kagebot
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import org.javacord.api.entity.channel.ServerTextChannel
import org.javacord.api.entity.message.embed.EmbedBuilder
import org.javacord.api.entity.server.Server
import org.javacord.api.event.message.MessageCreateEvent
import java.util.*
object TestUtil {
fun mockMessage(
@ -13,6 +19,7 @@ object TestUtil {
): MessageCreateEvent {
val message = mockk<MessageCreateEvent>()
every { message.messageContent } returns content
every { message.readableMessageContent } returns content
every { message.messageAuthor.id } returns author
every { message.channel.sendMessage(capture(capturedCalls)) } returns mockk()
every { message.messageAuthor.isYourself } returns isSelf
@ -20,4 +27,16 @@ object TestUtil {
every { message.messageAuthor.isBotOwner } returns false
return message
}
fun prepareServerConfig(sentMessages: MutableList<EmbedBuilder> = mutableListOf()) {
val channelMock = mockk<ServerTextChannel>()
every { channelMock.sendMessage(capture(sentMessages)) } returns mockk()
val resultMock = mockk<Optional<ServerTextChannel>>()
every { resultMock.isPresent } returns true
every {resultMock.get()} returns channelMock
val server = mockk<Server>()
every { server.icon.ifPresent(any()) } just Runs
every { server.getTextChannelById(any<Long>()) } returns resultMock
Config.server = server
}
}