Skip to content

Commit

Permalink
refactor: add docs to Command.kt and tweak properties
Browse files Browse the repository at this point in the history
  • Loading branch information
averen committed Jul 12, 2024
1 parent b614dce commit a3edcb1
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Help : Command(arguments = "[command]") {
if (event.argumentString.isNotEmpty()) {
val command = event.arguments.command()
// additionally respond with "not found" if the command can't or shouldn't be listed in /commands
if (command == null || (command.ownerOnly && !event.isOwner) || command.category == Category.CUSTOM) {
if (command == null || (command.isOwnerOnly && !event.isOwner) || command.category == Category.CUSTOM) {
event.replyError(event.get("not_found")).setEphemeral(true).queue()
return
}
Expand Down
105 changes: 82 additions & 23 deletions src/main/kotlin/com/sandrabot/sandra/entities/Command.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,51 +17,110 @@
package com.sandrabot.sandra.entities

import com.sandrabot.sandra.events.CommandEvent
import com.sandrabot.sandra.managers.CommandManager
import net.dv8tion.jda.api.Permission
import kotlin.reflect.full.createInstance
import kotlin.reflect.full.isSubclassOf

/**
* This class defines, configures, and handles the execution of slash commands.
*
* All commands are dynamically loaded at startup by the [CommandManager] using reflections.
* Any class within the `com.sandrabot.sandra.commands` package that extends this class will be initialized.
*/
@Suppress("MemberVisibilityCanBePrivate")
abstract class Command(
arguments: String = "",
val subgroup: String? = null,
val guildOnly: Boolean = false,
val group: String? = null,
val requiredPermissions: Set<Permission> = emptySet(),
val userPermissions: Set<Permission> = emptySet()
val selfPermissions: Set<Permission> = emptySet(),
val userPermissions: Set<Permission> = emptySet(),
) {

// this is open for when command names differ from their class names, like 8ball
/**
* Represents the ID that Discord has assigned to
* the slash command associated with this command.
*
* This field may only be set once.
*/
var id: Long = 0L
set(value) {
// only allow the field to be set once for top level commands
if (field == 0L && !isSubcommand) field = value
else throw IllegalStateException("Command ID for $path has already been set to $field")
}

/**
* Represents the name of this command. The default name is derived
* from the command's simple class name. Will always be lowercase.
*
* You may override this property if the command name must
* differ from the class name, such as 8ball vs Magic8Ball.
*/
open val name: String = this::class.simpleName!!.lowercase()

/**
* List of command arguments that were compiled successfully.
*
* @see Argument#compileArguments
*/
val arguments: List<Argument> = compileArguments(arguments)

/**
* Represents the category this command belongs to.
*
* @see Category
*/
val category: Category = Category.fromClass(this::class)
val ownerOnly: Boolean = category == Category.OWNER

/**
* Determines if this command should only be used by developers.
* Automatically determined based on the command's category.
*/
val isOwnerOnly: Boolean = category == Category.OWNER

/**
* List of this command's immediate subcommands.
*/
val subcommands: List<Command> = this::class.nestedClasses.filter { it.isSubclassOf(Command::class) }
.map { (it.createInstance() as Command).also { child -> child.parent = this } }

/**
* Lists all nested subcommands within this command.
*/
val allSubcommands: List<Command> = subcommands + subcommands.flatMap { it.allSubcommands }

// Must be lazy so parents are set before access
/**
* Allows nested subcommands to be recursively linked to the top level command.
*/
var parent: Command? = null
private set

/**
* Determines if this command is a subcommand or not.
*/
val isSubcommand: Boolean by lazy { parent != null }

/**
* Represents the command's full pathname.
*/
val path: String by lazy {
if (!isSubcommand) name else {
var topLevelParent = this
do topLevelParent = topLevelParent.parent ?: break while (true)
if (isSubcommand) {
var topLevel = this
do topLevel = topLevel.parent ?: break while (true)
buildString {
append(topLevelParent.name)
if (group != null) append('.', group)
append(topLevel.name)
if (subgroup != null) append('.', subgroup)
append('.', name)
}
}
} else name
}

var id: Long = 0L
set(value) {
// Only allow the field to be set once for top level commands
if (field == 0L && !isSubcommand) field = value
else throw IllegalStateException("Command ID for $path has already been set to $field")
}
var parent: Command? = null
internal set
val isSubcommand: Boolean
get() = parent != null

/**
* Executes the command with the given [CommandEvent].
*
* @param event The object containing information about the command context.
*/
abstract suspend fun execute(event: CommandEvent)

override fun toString(): String = "Command:$path"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ class InteractionListener(private val sandra: Sandra) : CoroutineEventListener {
// do additional checks for guild commands, since discord added privileges we only need to check for ourselves
if (slashEvent.isFromGuild) {
// make sure we have all the permissions we'll need to run this command
val allPermissions = basePermissions + command.requiredPermissions
val allPermissions = basePermissions + command.selfPermissions
allPermissions.find { event.isMissingPermission(it) }?.let {
event.replyError(event.missingPermissionMessage(it, self = true)).setEphemeral(true).queue()
return
}
}
if (command.ownerOnly && !event.isOwner) {
if (command.isOwnerOnly && !event.isOwner) {
event.replyError(event.getAny("core.owner_only")).setEphemeral(true).queue()
return
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 Avery Carroll and Logan Devecka
* Copyright 2017-2024 Avery Carroll and Logan Devecka
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -48,7 +48,7 @@ class ReadyListener(private val sandra: Sandra) : CoroutineEventListener {
if (sandra.settings.commandUpdates) try {
// update the global slash command list, this makes sure the commands match our local commands
val topCommands = sandra.commands.values.filterNot { it.isSubcommand }
val (owner, global) = topCommands.partition { it.ownerOnly }.toList().map { list ->
val (owner, global) = topCommands.partition { it.isOwnerOnly }.toList().map { list ->
list.map { command -> sandra.commands.commandData[command.path] }
}
val globalCommands = event.jda.updateCommands().addCommands(global).await()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2022 Avery Carroll and Logan Devecka
* Copyright 2017-2024 Avery Carroll and Logan Devecka
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -39,6 +39,7 @@ class CommandManager {
/**
* Shorthand for retrieving the list of current commands.
* Please note this collection is **mutable**, any changes should be done with care.
* Access is only provided with the intention of being used from eval prompts.
*/
val values: MutableCollection<Command>
get() = commands.values
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/sandrabot/sandra/utils/CommandData.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017-2023 Avery Carroll and Logan Devecka
* Copyright 2017-2024 Avery Carroll and Logan Devecka
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,7 +43,7 @@ fun Command.asCommandData(): SlashCommandData? {
// add the argument data here if applicable
if (arguments.isNotEmpty()) data.addOptions(arguments.map { it.asOptionData(path) })
// process any subcommands this command may have
if (allSubcommands.isNotEmpty()) allSubcommands.groupBy { it.group }.forEach { (group, commands) ->
if (allSubcommands.isNotEmpty()) allSubcommands.groupBy { it.subgroup }.forEach { (group, commands) ->
val subcommandData = commands.map { subcommand ->
val subPath = "commands.${subcommand.path}"
// retrieve a map of all possible names and descriptions
Expand Down

0 comments on commit a3edcb1

Please sign in to comment.