Thursday, September 19, 2024 11:59:29 PM
> settings

Customize


Authenticate

> embed.rb
# frozen_string_literal: true

module ESM
  class Embed
    EMPTY_SPACE = "\u200B"
    TAB = "#{EMPTY_SPACE}#{EMPTY_SPACE}#{EMPTY_SPACE}#{EMPTY_SPACE}"

    # Attributes that are available for building via Hash
    ATTRIBUTES = %i[author title description color fields]

    module Limit
      TITLE_LENGTH_MAX = 256
      DESCRIPTION_LENGTH_MAX = 2048
      FIELD_NAME_LENGTH_MAX = 256
      FIELD_VALUE_LENGTH_MAX = 1024
    end

    ###########################
    # Class methods
    ###########################

    #
    # Creates an embed from a preset
    #
    # @param type [Symbol] Template to build. Valid options: :info, :success, :error
    # @param ** [Hash] Any embed attributes to set
    # @param & [Block, nil] optional block that yields the new embed
    #
    # @return [ESM::Embed] The newly built instance
    #
    def self.build(type = nil, **, &)
      ESM::Embed.new(type, **, &)
    end

    #
    # Creates an embed from a Hash
    #
    # @param hash [Hash]
    # @param &block [Block, nil] Optional block that yields the new embed
    #
    # @return [ESM::Embed] The newly built instance
    #
    def self.from_hash(hash, &block)
      new do |embed|
        ###########
        # Author
        # Supports string or HashMap with full options
        if (author = hash[:author]) && author.present?
          author = ESM::Arma::HashMap.from(author).presence || {name: author}
          author = author.slice(:name, :url, :icon_url).symbolize_keys
          embed.set_author(**author)
        end

        ###########
        # Title
        if (title = hash[:title]) && title.present?
          embed.title = title.to_s
        end

        ###########
        # Description
        if (description = hash[:description]) && description.present?
          embed.description = description.to_s
        end

        ###########
        # Color
        color = hash[:color]
        embed.color =
          if ESM::Regex::HEX_COLOR.match?(color)
            color
          elsif color && ESM::Color::Toast.const_defined?(color.upcase)
            ESM::Color::Toast.const_get(color.upcase)
          else
            ESM::Color.random
          end

        ###########
        # Fields
        if (fields = hash[:fields]) && fields.is_a?(Array)
          fields.each do |field|
            case field
            when Hash
              name = field[:name].to_s
              value = field[:value]
              inline = field[:inline] || false
            when Array
              name, value, inline = field
              inline ||= false
            else
              next
            end

            # Transform the hash keys/values into a "list"
            value =
              if value.is_a?(Hash)
                value.map_join("\n") do |key, value|
                  "**#{key.humanize(keep_id_suffix: true)}:** #{value}"
                end
              else
                value.to_s
              end

            embed.add_field(name:, value:, inline:)
          end
        end

        yield(embed) if block
      end
    end

    ###########################
    # Instance methods
    ###########################
    attr_reader :title, :description, :image, :thumbnail, :footer, :color
    attr_accessor :fields, :author, :url, :timestamp

    def initialize(type = nil, attributes = {}, &block)
      @title = nil
      @description = nil
      @fields = []
      @footer = nil
      @author = nil
      @image = nil
      @thumbnail = nil
      @color = ESM::Color.random
      @url = nil
      @timestamp = DateTime.now

      if block
        yield(self)
      else
        build_from_template(type, **attributes)
      end
    end

    def title=(text)
      text ||= ""

      @title = text.truncate(Limit::TITLE_LENGTH_MAX, separator: " ")
    end

    def description=(text)
      text ||= ""

      text = text.join("\n") if text.is_a?(Array)
      @description = text.truncate(Limit::DESCRIPTION_LENGTH_MAX)
    end

    def add_field(value:, name: nil, inline: false)
      # This will make the name appear empty
      name = EMPTY_SPACE if name.nil?

      # Discord won't send messages that have an empty field. This forces the value to be appear empty, and Discord will accept it.
      value = EMPTY_SPACE if value.blank?

      if value.is_a?(Array)
        add_field_array(name:, values: value, inline:)
      else
        store_field(name:, value:, inline:)
      end

      self
    end

    def set_footer(text: nil, icon_url: nil)
      @footer = Discordrb::Webhooks::EmbedFooter.new(text: text, icon_url: icon_url)
    end

    def footer=(text)
      set_footer(text: text)
    end

    def set_author(name:, url: nil, icon_url: nil)
      @author = Discordrb::Webhooks::EmbedAuthor.new(name: name, url: url, icon_url: icon_url)
    end

    def image=(url)
      @image = Discordrb::Webhooks::EmbedImage.new(url: url)
    end

    def thumbnail=(url)
      @thumbnail = Discordrb::Webhooks::EmbedThumbnail.new(url: url)
    end

    def color=(color)
      @color =
        if color.is_a?(Symbol) && ESM::Color::Toast.const_defined?(color.to_s.upcase)
          ESM::Color::Toast.const_get(color.to_s.upcase)
        else
          color
        end
    end

    def transfer(embed)
      # And you can't do `embed = new_embed`
      embed.title = title if title
      embed.description = description if description
      embed.url = url if url
      embed.timestamp = timestamp if timestamp
      embed.color = color if color
      embed.footer = footer if footer
      embed.image = image if image
      embed.thumbnail = thumbnail if thumbnail
      embed.author = author if author
      embed.fields = fields if fields

      self
    end

    def to_s
      output = ""
      output += "Title (#{title.size}): #{title}\n" if title
      output += "Description (#{description.size}): #{description}\n" if description

      if fields
        output += "Fields:\n"
        fields.each_with_index do |field, index|
          output += "\t##{index + 1}"
          output += " <inline>" if field.inline

          output += "\n\t  Name (#{field.name.size}): #{field.name}"
          output += "\n\t  Value (#{field.value.size}): #{field.value}\n"
        end
      end

      # Add the metadata
      if metadata?
        output += "Metadata:\n"
        output += "\tTimestamp: #{timestamp}\n" if timestamp
        output += "\tColor: #{color}\n" if color
        output += "\tImage: #{image.url}\n" if image
        output += "\tThumbnail: #{thumbnail.url}\n" if thumbnail
        output += "\tURL: #{url}\n" if url
        output += "\tFooter: #{footer.text}" if footer
      end

      output
    end

    def to_h
      {
        title: title,
        description: description,
        timestamp: timestamp,
        color: color,
        footer: footer&.text,
        fields: fields.map { |f| {name: f.name, value: f.value, inline: f.inline} },
        author: author&.to_hash,
        thumbnail: thumbnail,
        image: image,
        url: url
      }
    end

    def for_discord_embed
      {
        title: title,
        description: description,
        timestamp: timestamp.to_s,
        color: color&.sub("#", "")&.to_i(16),
        footer: footer&.to_hash,
        fields: fields.map(&:to_hash),
        author: author&.to_hash,
        thumbnail: thumbnail&.to_hash,
        image: image&.to_hash,
        url: url&.to_hash
      }
    end

    def build_from_template(type, **attributes)
      case type
      when :info
        self.color = :blue
      when :error
        self.color = :red
      when :success
        self.color = :green
      end

      attributes.each do |attr_name, attr_value|
        send(:"#{attr_name}=", attr_value)
      end
    end

    private

    def metadata?
      timestamp.present? || color.present? || image.present? || thumbnail.present? || url.present?
    end

    def add_field_array(name:, values:, inline:)
      field_values = if values.sum(&:size) < Limit::FIELD_VALUE_LENGTH_MAX
        [values]
      else
        field_values = []
        field_counter = 0

        values.each do |value|
          field_content = field_values[field_counter] ||= []

          # If this value is too large for the current field, redo this iteration again but with a new field
          if (field_content.total_size + value.size) >= Limit::FIELD_VALUE_LENGTH_MAX
            field_counter += 1
            redo
          end

          field_content << value
        end

        field_values
      end

      field_values.each_with_index do |values, index|
        store_field(name: index.zero? ? name : EMPTY_SPACE, value: values.join("\n\n"), inline: inline)
      end
    end

    def store_field(name:, value:, inline:)
      # to_s to ensure a string
      @fields << Discordrb::Webhooks::EmbedField.new(
        name: name.to_s.truncate(Limit::FIELD_NAME_LENGTH_MAX, separator: "\n"),
        value: value.to_s.truncate(Limit::FIELD_VALUE_LENGTH_MAX, separator: "\n"),
        inline: inline
      )
    end
  end
end
All opinions represented herein are my own
- © 2024 itsthedevman
- build 3c15a1b