io.github.freya022.botcommands.internal.commands.application.ApplicationCommandsUpdater.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of BotCommands Show documentation
Show all versions of BotCommands Show documentation
A Kotlin-first (and Java) framework that makes creating Discord bots a piece of cake, using the JDA library.
package io.github.freya022.botcommands.internal.commands.application
import dev.minn.jda.ktx.coroutines.await
import io.github.freya022.botcommands.api.commands.application.AbstractApplicationCommandManager
import io.github.freya022.botcommands.api.commands.application.CommandScope
import io.github.freya022.botcommands.api.commands.application.GlobalApplicationCommandManager
import io.github.freya022.botcommands.api.commands.application.GuildApplicationCommandManager
import io.github.freya022.botcommands.api.core.service.getService
import io.github.freya022.botcommands.api.core.utils.overwriteBytes
import io.github.freya022.botcommands.internal.commands.application.ApplicationCommandsCache.Companion.toJsonBytes
import io.github.freya022.botcommands.internal.commands.application.context.message.MessageCommandInfo
import io.github.freya022.botcommands.internal.commands.application.context.user.UserCommandInfo
import io.github.freya022.botcommands.internal.commands.application.localization.BCLocalizationFunction
import io.github.freya022.botcommands.internal.commands.application.mixins.ITopLevelApplicationCommandInfo
import io.github.freya022.botcommands.internal.commands.application.slash.SlashSubcommandGroupInfo
import io.github.freya022.botcommands.internal.commands.application.slash.SlashSubcommandInfo
import io.github.freya022.botcommands.internal.commands.application.slash.SlashUtils.getDiscordOptions
import io.github.freya022.botcommands.internal.commands.application.slash.TopLevelSlashCommandInfo
import io.github.freya022.botcommands.internal.commands.mixins.INamedCommand
import io.github.freya022.botcommands.internal.core.BContextImpl
import io.github.freya022.botcommands.internal.utils.asScopeString
import io.github.freya022.botcommands.internal.utils.rethrowUser
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.dv8tion.jda.api.entities.Guild
import net.dv8tion.jda.api.interactions.commands.Command
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions
import net.dv8tion.jda.api.interactions.commands.build.CommandData
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData
import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction
import java.nio.file.Files
internal class ApplicationCommandsUpdater private constructor(
private val context: BContextImpl,
private val guild: Guild?,
manager: AbstractApplicationCommandManager
) {
private val logger = KotlinLogging.logger { }
private val commandsCache = context.getService()
private val onlineCheck = context.applicationConfig.onlineAppCommandCheckEnabled
private val commandsCachePath = when (guild) {
null -> commandsCache.globalCommandsPath
else -> commandsCache.getGuildCommandsPath(guild)
}
val applicationCommands: Collection
private val allCommandData: Collection
init {
Files.createDirectories(commandsCachePath.parent)
applicationCommands = manager.applicationCommands.values
allCommandData = computeCommands().allCommandData
//Apply localization
val localizationFunction: LocalizationFunction = BCLocalizationFunction(context)
for (commandData in allCommandData) {
commandData.setLocalizationFunction(localizationFunction)
}
}
suspend fun shouldUpdateCommands(): Boolean {
val oldBytes = when {
onlineCheck -> {
(guild?.retrieveCommands(true) ?: context.jda.retrieveCommands(true))
.await()
.map { CommandData.fromCommand(it) }.toJsonBytes()
}
else -> {
if (Files.notExists(commandsCachePath)) {
logger.trace { "Updating commands because cache file does not exists" }
return true
}
withContext(Dispatchers.IO) {
Files.readAllBytes(commandsCachePath)
}
}
}
val newBytes = allCommandData.toJsonBytes()
return (!ApplicationCommandsCache.isJsonContentSame(context, oldBytes, newBytes)).also { needUpdate ->
if (needUpdate) {
logger.trace { "Updating commands because content is not equal" }
if (context.debugConfig.enableApplicationDiffsLogs) {
logger.trace { "Old commands bytes: ${oldBytes.decodeToString()}" }
logger.trace { "New commands bytes: ${newBytes.decodeToString()}" }
}
}
}
}
suspend fun updateCommands() {
val updateAction = guild?.updateCommands() ?: context.jda.updateCommands()
val commands = updateAction
.addCommands(allCommandData)
.await()
saveCommandData(guild)
printPushedCommandData(commands, guild)
}
private fun computeCommands() = ApplicationCommandDataMap().also { map ->
computeSlashCommands(applicationCommands, map)
computeContextCommands(applicationCommands, map, UserCommandInfo::class.java, Command.Type.USER)
computeContextCommands(applicationCommands, map, MessageCommandInfo::class.java, Command.Type.MESSAGE)
}
private fun computeSlashCommands(guildApplicationCommands: Collection, map: ApplicationCommandDataMap) {
guildApplicationCommands
.filterIsInstance()
.filterCommands()
.forEach { info: TopLevelSlashCommandInfo ->
try {
val isTopLevel = info.isTopLevelCommandOnly()
val topLevelData = Commands.slash(info.name, info.description).also { commandData ->
if (isTopLevel) {
commandData.addOptions(info.getDiscordOptions(guild))
}
commandData.configureTopLevel(info)
}
topLevelData.addSubcommandGroups(info.subcommandGroups.values.filterCommands().mapToSubcommandGroupData())
topLevelData.addSubcommands(info.subcommands.values.filterCommands().mapToSubcommandData())
map[Command.Type.SLASH, info.name] = topLevelData
} catch (e: Exception) { //TODO use some sort of exception context for command paths
rethrowUser(info.function, "An exception occurred while processing command '${info.name}'", e)
}
}
}
private fun Collection.mapToSubcommandGroupData() =
this.map { subcommandGroupInfo ->
SubcommandGroupData(subcommandGroupInfo.name, subcommandGroupInfo.description).also {
it.addSubcommands(subcommandGroupInfo.subcommands.values.mapToSubcommandData())
}
}
private fun Collection.mapToSubcommandData() =
this.map { subcommandInfo ->
SubcommandData(subcommandInfo.name, subcommandInfo.description)
.addOptions(subcommandInfo.getDiscordOptions(guild))
}
private fun computeContextCommands(
guildApplicationCommands: Collection,
map: ApplicationCommandDataMap,
targetClazz: Class,
type: Command.Type
) where T : ITopLevelApplicationCommandInfo,
T : ApplicationCommandInfo {
guildApplicationCommands
.filterIsInstance(targetClazz)
.filterCommands()
.forEach { info: T ->
try {
//Standard command
map[type, info.name] = Commands.context(type, info.name).configureTopLevel(info)
} catch (e: Exception) {
rethrowUser(info.function, "An exception occurred while processing a ${type.name} command ${info.name}", e)
}
}
}
private fun Collection.filterCommands() = filter { info ->
context.settingsProvider?.let { settings ->
guild?.let { guild ->
return@filter settings.getGuildCommands(guild).filter.test(info.path)
}
}
return@filter true
}
private fun CommandData.configureTopLevel(info: T): CommandData
where T : ITopLevelApplicationCommandInfo,
T : ApplicationCommandInfo = apply {
if (info.nsfw) isNSFW = true
if (info.scope == CommandScope.GLOBAL_NO_DM) isGuildOnly = true
if (info.isDefaultLocked) {
defaultPermissions = DefaultMemberPermissions.DISABLED
} else if (info.userPermissions.isNotEmpty()) {
defaultPermissions = DefaultMemberPermissions.enabledFor(info.userPermissions)
}
}
private fun printPushedCommandData(commands: List, guild: Guild?) {
if (!logger.isTraceEnabled()) return
logger.trace {
val commandNumber = commands.size
val sentCommandNumber = allCommandData.size
val scope = guild.asScopeString()
"Updated $commandNumber / $sentCommandNumber commands for $scope"
}
}
private fun saveCommandData(guild: Guild?) {
try {
commandsCachePath.overwriteBytes(allCommandData.toJsonBytes())
} catch (e: Exception) {
logger.error(e) {
"An exception occurred while temporarily saving ${guild.asScopeString()} commands in '${commandsCachePath.toAbsolutePath()}'"
}
}
}
companion object {
fun ofGlobal(context: BContextImpl, manager: GlobalApplicationCommandManager): ApplicationCommandsUpdater {
return ApplicationCommandsUpdater(context, null, manager)
}
fun ofGuild(context: BContextImpl, guild: Guild, manager: GuildApplicationCommandManager): ApplicationCommandsUpdater {
return ApplicationCommandsUpdater(context, guild, manager)
}
}
}