# frozen_string_literal: true
module ESM
module Command
CATEGORIES = %w[
community
development
entertainment
general
server
system
].freeze
# @return [Array<Symbol>] A list of publicly available command types.
# Any types not in this list will not show up in the help documentation or on the website.
TYPES = %i[admin player].freeze
class << self
attr_reader :all, :by_type, :by_namespace
delegate :application_command, :register_application_command, to: "::ESM.discord_bot"
end
def self.[](command_name)
return if command_name.blank?
command_name = command_name.to_s unless command_name.is_a?(String)
command_name = command_name[1..] if command_name.start_with?("/")
# Find by name or by its usage
all.find do |command_class|
# OG command name check
next true if command_class.command_name == command_name
# Slash command usage check
usage = command_class.usage(with_slash: false, with_args: false)
next true if usage == command_name
# Slash command "subgroup" (the last part of the slash usage)
next true if usage.split(" ").last == command_name
end
end
def self.get(command_name)
self[command_name]
end
def self.admin_commands
by_type[:admin]
end
def self.player_commands
by_type[:player]
end
def self.include?(command_name)
!self[command_name].nil?
end
def self.load
@all = []
@by_type = []
@by_namespace = {}
ApplicationCommand.subclasses.each do |command_class|
# Background commands do not have types
next if command_class.type.nil?
@all << command_class
# Build the command structure for both server and global commands
# Commands must be registered with Discord together if they share the same namespace
register_namespace(command_class)
end
# Lock it!
@all.freeze
@by_type = @all.group_by(&:type).freeze
end
#
# Returns configurations for all commands, often used for database inserts
#
# @return [Array<Hash>]
#
def self.configurations
@configurations ||=
ESM::Command.all.map do |command_class|
cooldown_default = command_class.attributes[:cooldown_time]&.default || 2.seconds
case cooldown_default
when Enumerator, Integer
cooldown_type = "times"
cooldown_quantity = cooldown_default.size
when ActiveSupport::Duration
# Converts 2.seconds to [:seconds, 2]
cooldown_type, cooldown_quantity = cooldown_default.parts.to_a.first
else
raise TypeError, "Invalid type \"#{cooldown_default.class}\" detected for command #{command.name}'s default cooldown"
end
enabled =
if (define = command_class.attributes[:enabled])
define.default
else
true
end
allowed_in_text_channels =
if (define = command_class.attributes[:allowed_in_text_channels])
define.default
else
true
end
allowlist_enabled =
if (define = command_class.attributes[:allowlist_enabled])
define.default
else
command_class.type == :admin
end
allowlisted_role_ids =
if (define = command_class.attributes[:allowlisted_role_ids])
define.default
else
[]
end
{
command_name: command_class.command_name,
enabled: enabled,
cooldown_quantity: cooldown_quantity,
cooldown_type: cooldown_type,
allowed_in_text_channels: allowed_in_text_channels,
allowlist_enabled: allowlist_enabled,
allowlisted_role_ids: allowlisted_role_ids
}
end
end
#
# Using the namespaces of the commands, this method registers the event_hook with Discordrb so it's executed
# when a command event occurs
#
def self.setup_event_hooks!
by_namespace.each do |name, segments_or_command|
# It's a command and it's at the root level
if all.include?(segments_or_command)
application_command(name, &segments_or_command.method(:event_hook))
next
end
# It's a group and it may have subgroups or subcommands
segments_or_command.each do |sub_name, segments_or_command|
# It's a command!
if all.include?(segments_or_command)
application_command(name).subcommand(sub_name, &segments_or_command.method(:event_hook))
next
end
# It's a group!
application_command(name).group(sub_name) do |group|
segments_or_command.each do |command_name, command|
group.subcommand(command_name, &command.method(:event_hook))
end
end
end
end
nil
end
#
# Registers admin and development commands as "server commands" for the community
# These are only present on the Discord and are not global
#
# @param community_discord_id [String, Integer, NilClass] The community's guild ID to register the command to the
# the community. Otherwise, it'll register it globally
#
def self.register_commands(community_discord_id = nil)
by_namespace.each do |name, segments_or_command|
register_command(name, segments_or_command, community_discord_id)
end
nil
end
# @!visibility private
def self.register_command(name, segments_or_command, community_discord_id)
# It's a command and it's at the root level
if all.include?(segments_or_command)
segments_or_command.register_root_command(community_discord_id, name)
return
end
# It's a group and it may have subgroups or subcommands
::ESM.discord_bot.register_application_command(name, "C - If you are seeing this, something went wrong", server_id: community_discord_id) do |builder|
segments_or_command.each do |name, segments_or_command|
# It's a command!
if all.include?(segments_or_command)
segments_or_command.register_subcommand(builder, name)
next
end
# It's a group!
builder.subcommand_group(name, "G - If you are seeing this, something went wrong") do |group_builder|
segments_or_command.each do |command_name, command|
command.register_subcommand(group_builder, command_name)
end
end
end
end
end
# @!visibility private
private_class_method def self.register_namespace(command_class)
namespace = command_class.namespace
segments = namespace[:segments]
command_name = namespace[:command_name]
current_segment = @by_namespace
# Discord treats the first segment after the slash as the "command name"
# Commands may use this as the root namespace (/help, /register)
# Or this could be a single group (/community <command_name>)
if (segment = segments.first)
current_segment = current_segment[segment.to_sym] ||= {}
end
# Discord treats this as a "subgroup" after the "command name"
# Commands may use this as a group namespace (/server admin <command_name>)
if (segment = segments.second)
current_segment = current_segment[segment.to_sym] ||= {}
end
# Ensure this namespace isn't already used
if (namespaced_command = current_segment[command_name.to_sym])
raise ESM::Exception::InvalidCommandNamespace,
"#{command_class} attempted to bind to the namespace `/#{segments.join(" ")} #{command_name}` which is already bound to #{namespaced_command}"
end
# Finally store the command with the final segment (/server admin show)
current_segment[command_name.to_sym] = command_class
end
end
end