Skip to content
140 changes: 129 additions & 11 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/CommandListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import at.hannibal2.skyhanni.discord.command.PullRequestCommand
import at.hannibal2.skyhanni.discord.command.ServerCommands
import at.hannibal2.skyhanni.discord.command.TagCommands
import at.hannibal2.skyhanni.discord.command.TagUndo
import net.dv8tion.jda.api.entities.Guild
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData
import org.reflections.Reflections
import java.lang.reflect.Modifier

Expand All @@ -23,16 +29,25 @@ object CommandListener {

fun init() {
loadCommands()
BOT.jda.getGuildById(BOT.config.allowedServerId)?.let { createCommands(it) }
}

fun onMessage(bot: DiscordBot, event: MessageReceivedEvent) {
event.onMessage(bot)
}

fun onInteraction(bot: DiscordBot, event: SlashCommandInteractionEvent) {
event.onInteraction(bot)
}

fun onAutocomplete(event: CommandAutoCompleteInteractionEvent) {
event.onCompletion()
}

private fun MessageReceivedEvent.onMessage(bot: DiscordBot) {
val message = message.contentRaw.trim()
if (!isFromGuild) {
logAction("private dm: '$message'")
logAction("private dm: '$message'", this)
return
}
if (guild.id != bot.config.allowedServerId) return
Expand Down Expand Up @@ -63,33 +78,110 @@ object CommandListener {
}

if (!command.userCommand) {
if (!hasAdminPermissions()) {
reply("No permissions $PLEADING_FACE")
if (!hasAdminPermissions(this)) {
reply("No permissions $PLEADING_FACE", this)
return
}

if (!inBotCommandChannel()) {
reply("Wrong channel $PLEADING_FACE")
if (!inBotCommandChannel(this)) {
reply("Wrong channel $PLEADING_FACE", this)
return
}
}

// allows to use `!<command> -help` instaed of `!help -<command>`
// allows to use `!<command> -help` instead of `!help -<command>`
if (args.size == 1 && args.first() == "-help") {
with(HelpCommand) {
sendUsageReply(literal)
sendUsageReply(literal, this@onMessage)
}
return
}
// allows to use `!<command> -help` instead of `!help -<command>`
if (args.size == 1) {
if (args.first() == "-help") {
with(HelpCommand) {
this.sendUsageReply(literal, this@onMessage)
}
return
}
}
try {
with(command) {
execute(args)
execute(args, this@onMessage)
}
} catch (e: Exception) {
reply("Error: ${e.message}", this)
}
}

private fun SlashCommandInteractionEvent.onInteraction(bot: DiscordBot) {
if (guild?.id != bot.config.allowedServerId || this.user.isBot) return

val command = getCommand(this.fullCommandName) ?: return

if (!command.userCommand) {
if (!hasAdminPermissions(this)) {
reply("No permissions $PLEADING_FACE")
return
}

if (!inBotCommandChannel(this)) {
reply("Wrong channel $PLEADING_FACE")
return
}
}

try {
with(command) {
execute(listOf(), this@onInteraction)
}
} catch (e: Exception) {
reply("Error: ${e.message}")
}
}

private fun CommandAutoCompleteInteractionEvent.onCompletion() {
when (fullCommandName) {
"help" -> {
if (focusedOption.name != "command") return

replyChoiceStrings(
commands.filter { it.name.startsWith(focusedOption.value) }
.map { it.name }
.take(25)
).queue()
}

"server" -> {
if (focusedOption.name != "keyword") return

replyChoiceStrings(
ServerCommands.servers.filter { it.name.startsWith(focusedOption.value, true) }
.map { it.name }
.take(25)
).queue()
}

"tagedit" -> {
if (focusedOption.name != "keyword") return

replyChoiceStrings(
Database.listKeywords().filter { it.startsWith(focusedOption.value, true) }
.take(25)
).queue()
}

"tagdelete" -> {
if (focusedOption.name != "keyword") return

replyChoiceStrings(
Database.listKeywords().filter { it.startsWith(focusedOption.value, true) }
.take(25)
).queue()
}
}
}

private val commandPattern = "^!(?!!).+".toPattern()

// ensures the command starts with ! while ignoring !!
Expand Down Expand Up @@ -118,9 +210,35 @@ object CommandListener {
e.printStackTrace()
}
}
this.commands = commands
this.commandsMap = commandsMap
}

fun createCommands(guild: Guild) {
guild.retrieveCommands().queue {
val commandData = commands.map { value -> convertToData(value) }

guild.updateCommands().addCommands(commandData).queue()
}
}

private fun convertToData(old: BaseCommand): SlashCommandData {
return Commands.slash(old.name, old.description).apply {
old.options.forEach { option ->
addOption(
option.type,
option.name.replace(" ", "_"),
option.description,
option.required,
option.autoComplete
)
}
}
}
}

data class Option(val name: String, val description: String, val required: Boolean = true)
data class Option(
val name: String,
val description: String,
val required: Boolean = true,
val type: OptionType = OptionType.STRING,
val autoComplete: Boolean = false
)
23 changes: 22 additions & 1 deletion src/main/kotlin/at/hannibal2/skyhanni/discord/DiscordBot.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package at.hannibal2.skyhanni.discord
import at.hannibal2.skyhanni.discord.Utils.sendMessageToBotChannel
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.events.session.ReadyEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.requests.GatewayIntent
import org.slf4j.Logger
Expand Down Expand Up @@ -68,12 +71,30 @@ private fun startBot(): DiscordBot {
}.build()

val bot = DiscordBot(jda, config)
jda.awaitReady()
val messageListener = object : ListenerAdapter() {
override fun onMessageReceived(event: MessageReceivedEvent) {
CommandListener.onMessage(bot, event)
}
}
val slashCommandListener = object : ListenerAdapter() {
override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) {
CommandListener.onInteraction(bot, event)
}

override fun onCommandAutoCompleteInteraction(event: CommandAutoCompleteInteractionEvent) {
CommandListener.onAutocomplete(event)
}

override fun onReady(event: ReadyEvent) {
event.jda.getGuildById(config.allowedServerId)?.let {
CommandListener.createCommands(it)
}
}
}
jda.addEventListener(slashCommandListener)
jda.addEventListener(messageListener)

jda.awaitReady()

return bot
}
99 changes: 77 additions & 22 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/Utils.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package at.hannibal2.skyhanni.discord

import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.entities.Member
import net.dv8tion.jda.api.entities.Message
import net.dv8tion.jda.api.entities.MessageEmbed
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel
import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.utils.FileUpload
import org.slf4j.LoggerFactory
import java.awt.Color
import java.io.File
import java.util.zip.ZipFile
Expand All @@ -20,26 +21,43 @@ object Utils {

private inline val logger get() = BOT.logger

fun MessageReceivedEvent.reply(text: String) {
message.messageReply(text)
fun reply(text: String, event: Any, ephemeral: Boolean = false) {
doWhen(event, {
it.message.messageReply(text)
}, {
it.reply(text).setEphemeral(ephemeral).queue()
})
}

fun MessageReceivedEvent.userError(text: String) {
message.messageReply("❌ $text")
fun reply(embed: MessageEmbed, event: Any, ephemeral: Boolean = false) {
doWhen(event, {
it.message.messageReply(embed)
}, {
it.replyEmbeds(embed).setEphemeral(ephemeral).queue()
})
}

fun MessageReceivedEvent.sendError(text: String) {
message.messageReply("❌ An error occurred: $text")
logAction("Error: $text")
fun userError(text: String, event: Any, ephemeral: Boolean = true) {
doWhen(event, {
it.message.messageReply("❌ $text")
}, {
it.reply("❌ $text").setEphemeral(ephemeral).queue()
})
}

fun MessageReceivedEvent.reply(embed: MessageEmbed) {
message.messageReply(embed)
fun sendError(text: String, event: Any, ephemeral: Boolean = true) {
doWhen(event, {
it.message.messageReply("❌ $text")
}, {
it.reply("❌ $text").setEphemeral(ephemeral).queue()
})

logAction("Error: $text", event)
}

fun MessageReceivedEvent.replyWithConsumer(text: String, consumer: (MessageReceivedEvent) -> Unit) {
BotMessageHandler.log(text, consumer)
reply(text)
reply(text, this)
}

fun Message.messageDelete() {
Expand Down Expand Up @@ -82,31 +100,68 @@ object Utils {
BOT.jda.getTextChannelById(BOT.config.botCommandChannelId)?.messageSend(text, instantly)
}

fun MessageReceivedEvent.logAction(action: String, raw: Boolean = false) {
fun logAction(action: String, event: Any, raw: Boolean = false) {
val author = when (event) {
is MessageReceivedEvent -> event.author
is SlashCommandInteractionEvent -> event.user
else -> throw IllegalArgumentException("Unknown event type")
}

val member = when (event) {
is MessageReceivedEvent -> event.member
is SlashCommandInteractionEvent -> event.member
else -> throw IllegalArgumentException("Unknown event type")
}

val channel = when (event) {
is MessageReceivedEvent -> event.channel
is SlashCommandInteractionEvent -> event.channel
else -> throw IllegalArgumentException("Unknown event type")
}

if (raw) {
logger.info(action)
return
}

val name = author.name
val id = author.id
val nickString = member?.nickname?.takeIf { it != "null" }?.let { " (`$it`)" } ?: ""
val isFromGuild =
(event as? MessageReceivedEvent)?.isFromGuild ?: (event as? SlashCommandInteractionEvent)?.isFromGuild
?: false
val channelSuffix = if (isFromGuild) " in channel '${channel.name}'" else ""

val nick = member?.nickname?.takeIf { it != "null" }
val nickString = nick?.let { " (`$nick`)" } ?: ""

val channelSuffix = if (isFromGuild) {
val channelName = channel.name
" in channel '$channelName'"
} else ""
logger.info("$id/$name$nickString $action$channelSuffix")
}

fun MessageReceivedEvent.hasAdminPermissions(): Boolean {
val member = member ?: return false
fun hasAdminPermissions(event: Any): Boolean {
val member = doWhen(
event,
{ it.member },
{ it.member }
) as Member

val allowedRoleIds = BOT.config.editPermissionRoleIds.values
return !member.roles.none { it.id in allowedRoleIds }
}

fun MessageReceivedEvent.inBotCommandChannel() = channel.id == BOT.config.botCommandChannelId
fun <T> doWhen(
event: Any,
consumer: (MessageReceivedEvent) -> T?,
consumer2: (SlashCommandInteractionEvent) -> T?
): T? {
return when (event) {
is MessageReceivedEvent -> consumer(event)
is SlashCommandInteractionEvent -> consumer2(event)
else -> null
}
}

fun inBotCommandChannel(event: Any): Boolean {
val id = doWhen(event, { it.channel.id }, { it.channel.id })
return id == BOT.config.botCommandChannelId
}

fun runDelayed(duration: Duration, consumer: () -> Unit) {
Thread {
Expand Down
Loading