Friday, September 20, 2024 12:10:23 AM
> settings

Customize


Authenticate

> command.rb
# 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.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 = {}

      commands_needing_cached = []
      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)

        # Only admin and player commands need to be cached
        next unless TYPES.include?(command_class.type)

        # To be written to the DB in bulk
        commands_needing_cached << command_class.to_details
      end

      # Lock it!
      @all.freeze
      @by_type = @all.group_by(&:type).freeze

      # Run some jobs for command
      RebuildCommandCacheJob.perform_async(commands_needing_cached)
      SyncCommandConfigurationsJob.perform_async(nil)
      SyncCommandCountsJob.perform_async(nil)
    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.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
All opinions represented herein are my own
- © 2024 itsthedevman
- build 3c15a1b