Skip to content
157 changes: 139 additions & 18 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/CommandListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ package at.hannibal2.skyhanni.discord
import at.hannibal2.skyhanni.discord.Utils.hasAdminPermissions
import at.hannibal2.skyhanni.discord.Utils.inBotCommandChannel
import at.hannibal2.skyhanni.discord.Utils.logAction
import at.hannibal2.skyhanni.discord.Utils.reply
import at.hannibal2.skyhanni.discord.command.BaseCommand
import at.hannibal2.skyhanni.discord.command.HelpCommand
import at.hannibal2.skyhanni.discord.command.PullRequestCommand
import at.hannibal2.skyhanni.discord.command.BaseCommand
import at.hannibal2.skyhanni.discord.command.MessageEvent
import at.hannibal2.skyhanni.discord.command.SlashCommandEvent
import at.hannibal2.skyhanni.discord.command.ServerCommands
import at.hannibal2.skyhanni.discord.command.PullRequestCommand.isPullRequest
import at.hannibal2.skyhanni.discord.command.ServerCommands.isKnownServerUrl
import at.hannibal2.skyhanni.discord.command.TagCommands
import at.hannibal2.skyhanni.discord.command.TagCommands.handleTag
import at.hannibal2.skyhanni.discord.command.TagUndo
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.entities.Guild
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent
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,42 +30,51 @@ object CommandListener {

fun init() {
loadCommands()

BOT.jda.awaitReady()

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

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

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

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

private fun MessageEvent.onMessage(bot: DiscordBot) {
val message = message.contentRaw.trim()
if (!isFromGuild) {
logAction("private dm: '$message'")
return
}
if (guild.id != bot.config.allowedServerId) return
if (event.guild.id != bot.config.allowedServerId) return

if (this.author.isBot) {
if (this.author.id == BOT_ID) {
BotMessageHandler.handle(this)
if (author.isBot) {
if (author.id == BOT_ID) {
BotMessageHandler.handle(event)
}
return
}

if (TagUndo.getAllNames().none { "!$it" == message }) {
TagCommands.lastMessages.remove(this.author.id)
TagCommands.lastMessages.remove(author.id)
}

if (ServerCommands.isKnownServerUrl(this, message)) return
if (PullRequestCommand.isPullRequest(this, message)) return

if (!isCommand(message)) return
if (isKnownServerUrl(message) || isPullRequest(message) || !isCommand(message)) return

val split = message.substring(1).split(" ")
val literal = split.first().lowercase()
val args = split.drop(1)

val command = getCommand(literal) ?: run {
TagCommands.handleTag(this)
handleTag()
return
}

Expand All @@ -74,13 +90,22 @@ object CommandListener {
}
}

// 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)
}
return
}
// allows to use `!<command> -help` instead of `!help -<command>`
if (args.size == 1) {
if (args.first() == "-help") {
with(HelpCommand) {
sendUsageReply(literal)
}
return
}
}
try {
with(command) {
execute(args)
Expand All @@ -90,6 +115,74 @@ object CommandListener {
}
}

private fun SlashCommandEvent.onInteraction(bot: DiscordBot) {
if (event.guild?.id != bot.config.allowedServerId || author.isBot) return

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

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

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

try {
with(command) {
execute(listOf())
}
} 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.listTags().filter { it.keyword.startsWith(focusedOption.value, true) }.map { it.keyword }
.take(25)
).queue()
}

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

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

private val commandPattern = "^!(?!!)[\\s\\S]+".toPattern()

// ensures the command starts with ! while ignoring !!
Expand Down Expand Up @@ -121,6 +214,34 @@ object CommandListener {
this.commands = commands
this.commandsMap = commandsMap
}

private 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
)
20 changes: 18 additions & 2 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/DiscordBot.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package at.hannibal2.skyhanni.discord

import at.hannibal2.skyhanni.discord.Utils.sendMessageToBotChannel
import at.hannibal2.skyhanni.discord.command.MessageEvent
import at.hannibal2.skyhanni.discord.command.SlashCommandEvent
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.hooks.ListenerAdapter
import net.dv8tion.jda.api.requests.GatewayIntent
Expand Down Expand Up @@ -71,12 +75,24 @@ 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)
CommandListener.onMessage(bot, MessageEvent(event))
}
}
val slashCommandListener = object : ListenerAdapter() {
override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) {
CommandListener.onInteraction(bot, SlashCommandEvent(event))
}

override fun onCommandAutoCompleteInteraction(event: CommandAutoCompleteInteractionEvent) {
CommandListener.onAutocomplete(event)
}
}
jda.addEventListener(slashCommandListener)
jda.addEventListener(messageListener)

jda.awaitReady()

return bot
}
41 changes: 9 additions & 32 deletions src/main/kotlin/at/hannibal2/skyhanni/discord/Utils.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package at.hannibal2.skyhanni.discord

import at.hannibal2.skyhanni.discord.command.CommandEvent
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.entities.Message
import net.dv8tion.jda.api.entities.MessageEmbed
Expand All @@ -20,28 +21,6 @@ object Utils {

private inline val logger get() = BOT.logger

fun MessageReceivedEvent.reply(text: String) {
message.messageReply(text)
}

fun MessageReceivedEvent.userError(text: String) {
message.messageReply("❌ $text")
}

fun MessageReceivedEvent.sendError(text: String) {
message.messageReply("❌ An error occurred: $text")
logAction("Error: $text")
}

fun MessageReceivedEvent.reply(embed: MessageEmbed) {
message.messageReply(embed)
}

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

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

fun MessageReceivedEvent.logAction(action: String, raw: Boolean = false) {
fun CommandEvent.logAction(action: String, raw: Boolean = false) {
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 = isFromGuild
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 {
fun CommandEvent.hasAdminPermissions(): Boolean {
val member = member ?: return false

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

fun MessageReceivedEvent.inBotCommandChannel() = channel.id == BOT.config.botCommandChannelId
fun CommandEvent.inBotCommandChannel(): Boolean = channel.id == BOT.config.botCommandChannelId

fun runDelayed(duration: Duration, consumer: () -> Unit) {
Thread {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package at.hannibal2.skyhanni.discord.command

import at.hannibal2.skyhanni.discord.Option
import at.hannibal2.skyhanni.discord.Utils.userError
import net.dv8tion.jda.api.events.message.MessageReceivedEvent

abstract class BaseCommand {

Expand All @@ -16,9 +14,9 @@ abstract class BaseCommand {

protected open val aliases: List<String> = emptyList()

abstract fun MessageReceivedEvent.execute(args: List<String>)
abstract fun CommandEvent.execute(args: List<String>)

protected fun MessageReceivedEvent.wrongUsage(args: String) {
protected fun CommandEvent.wrongUsage(args: String) {
userError("Usage: `!$name $args`")
}

Expand Down
Loading