Skip to content

A data-driven Discord bot for creating custom message commands.

License

Notifications You must be signed in to change notification settings

Amgelo563/enki-bot

Repository files navigation

📚 Enki Bot

🇺🇸 Reading in English / 🇪🇸 Leer en Español

A powerful data-driven Discord bot for documenting resources and providing quick messages (tags), supporting categories, nested tags, resource/tag commands and more.

Table of Contents

🔨 Usage

Enki has two types of data: Tags and Resources. In this example we will be configuring Enki for a community where we offer products.


🏷 Tags

A Tag is a message (that can have content, embed and buttons) that is triggered by a command. They are loaded by Tag Categories, defined in the content/tag-atlas.conf file.

In Discord:

  • The tag category is loaded as a command with an option to select the tag.
  • Optionally, each tag inside the category can include an alias as a command.

Tip

For example, you can have a tag category for frequently asked questions in your server, or another one for videos, each on their own command (/faq <tag> and /video <tag>, respectively). PlantUML Tags Diagram

Tag Atlas File

The Tag Atlas is an array of tag categories, each of which define the path, the category command, and what data to use while autocompleting.

content/tag-atlas.conf

[
  {
    // Glob array to match tags from this category, relative to current directory (config).
    // See the "Globs" section for more information about globs.
    tags: ["tags/faq/**.conf"],
    
    // Command to use to search for tags of this category.
    command: {
      // Command Schema with "tag" option, for the tag to search
    },
    
    // What data to use to fuzzy search for tags while autocompleting, apart from the tag's keywords.
    searchBy: {
      // Whether to search by the tag's content.
      content: true,
      // Whether to search by the tag's embeds titles and descriptions.
      embeds: true
    },
    
    // A message to show when this command is called without any tags or an invalid one.
    // Don't include it or set it to `false` if you don't need it.
    // ^ On that case, the tag option will be required, and any invalid tags will show an error.
    message: {
      // Message Schema with buttons
    }
  }
]

Tag Files

After defining a tag category, you can now create your tags inside the path you provided.

content/tags/faq/my-question.conf

{
  // What keywords the user can use to trigger this tag, requires at least one. Will also be used for autocompletion.
  // Only the first keyword will be shown for autocompletion, typing the others will show the first one.
  // WARN: The first keyword will be used as the tag's ID. While more than one tag can have the same "secondary" keywords,
  //       the ID must be unique.
  keywords: ["my-question"],
  
  // Optionally, define a command that will trigger this command (an alias of /<category> <this tag>).
  // Don't include it or set it to `false` if you don't need it.
  command: {
    // Command schema without options.
    name: "my-question",
    description: "Command that triggers the my-question tag."
  },
  
  // Optional, a fancy tag name, shown alongside its message summary in autocompletion.
  displayName: "My Question",
  
  message: {
    // Message schema with buttons.
  }
}

You can then use:

  • /faq my-question - To trigger the "my-question" tag.
  • /my-question - Same as above, thanks to the command property in my-question.conf.

📖 Resources

A Resource is an object that contains tags and tag categories. They are defined in the content/resource-atlas.conf file.

In Discord:

  • Each resource is loaded as the main command.
    • "Direct" tags are loaded as subcommands.
    • Tag categories are loaded as subcommands (with an option to select the tag).
  • Optionally, each tag inside a tag category can include an alias as a subcommand (in the main command).

Tip

For example, you can have a "my-product" resource, letting you include videos, faqs, etc per product. PlantUML Resource diagram

Resource Atlas File

The resource atlas is located in the content/resources-atlas.conf file. It contains an array of resources.

[
  {
    // The entry point for this resource's tags and tag categories.
    command: {
      name: "my-product",
      description: "Command that triggers the my-product resource."
    },

    // Glob pattern array for the tags of this resource, relative to current path.
    // Warn: Every tag included here must have a command specified, which will be used as a subcommand.
    tags: ["my-product/**.conf"],

    // Follows the same format as a tag atlas
    categories: [
      {
        // Relative to current path.
        tags: [
          "my-product/faq/**.conf"
        ],
        // While the option is called command, it'd actually be a subcommand inside the resource's command
        command: {
          // Command schema with option "tag"
        },
        searchBy: {
          content: true,
          embeds: true
        },
        message: {
          // Message Schema with buttons
        }
      }
    ]
  }
]

Now you can use:

  • /my-product <tag> to query a tag from config/resources/my-product.
  • /my-product faq [tag] to query a tag from config/resources/my-product/faq.

📁 Globs

"Globs" are patterns to match files using special syntax. The following characters are the most common special ones.

Tip

You can view a more detailed explanation here. For context there, Enki only uses the { absolute: true } option.

  • * Matches 0 or more characters in a single path portion. For instance, faq/*.conf matches all .conf files only in the faq folder, not recursively.
  • ** Same as above, but matches recursively.
  • !(pattern|pattern) Matches anything that does not match any of the patterns provided. You can use it alongside another glob in the array to exclude files, for instance ["*.conf", "!*_ignore.conf"] will match every file, except those ending in _ignore.conf.
  • ? Matches any 1 character. For instance, faq/?.conf will match faq/A.conf, faq/B.conf, etc; but not faq/AB.conf (because it has two characters).

⌨ Schemas

There are multiple formats Enki re-uses across its configuration. These are called "Schemas".

💬 Message Schema

The message schema is used when configuring what message will be sent to the user.

{
  // WARNING: Even though every option is optional, either content or embeds must be provided (you can't omit both).

  // The message content, optional.
  content: "Showing faqs",

  // The message embeds, optional.
  // Check https://discord.com/developers/docs/resources/message#embed-object-embed-limits to see the character limits on embeds.
  // Enki will also make sure the embeds are valid, including the "sum of characters" part.
  // Every part is optional, but it needs to have at least a title, an author, a description, a thumbnail, 1 field, an image or a footer.
  embeds: [{
    title: "Embed title",
    description: "Embed Description",
    url: "https://embed.url/",

    // https://www.iso.org/iso-8601-date-and-time-format.html
    timestamp: "2024-07-08",

    color: "#FFFFFF",
    footer: {
      text: "Footer's text",
      // Optional
      icon: "https://footer-icon.url/",
    },
    image: "https://image.url/",
    thumbnail: "https://thumbnail.url/",
    author: {
      name: "Author's name",
      // Optional
      url: "https://author.url/",
      // Optional
      icon: "https://author-icon.url/",
    },
    fields: [
      {
        name: "Field 1 Name",
        value: "Field 1 Value",
        // Optional
        inline: false,
      },
      {
        name: "Field 2 Name",
        value: "Field 2 Value",
        inline: false,
      }
    ]
  }],

  // Array of file paths (not globs) to send those files alongside the message, optional.
  // These file paths can be:
  // - Relative to the current directory, if they start with ./ (e.g., "./relative/path/file.png").
  // - Absolute paths starting from root (folder containing src, README, etc), if they start with /
  //   (e.g., "/absolute/path/to/file.png").
  files: [
    "./relative/path/file.png",
    "/absolute/file.png"
  ],

  // The message buttons, optional.
  // WARNING: Not every message option will read buttons. Check the doc first.
  buttons: [
    {
      // Either "url", "tag" or "message" type. See next buttons for examples
      type: "url",
      // The button label.
      label: "Link 1",
      // The button's emoji, optional.
      emoji: "🔗", 
      // The button url.
      url: "https://example.com",
    },
    {
      type: "tag",
      label: "See related tag",
      emoji: "❓",
      // Tag reference schema, see the end to check all the available options.
      tag: {
        
      },
    },
    {
      type: "message",
      label: "See this other message",
      emoji: "👀",
      // Used internally, must be unique across messages of this button.
      id: "someMsg",
      message: {
        // Message schema without buttons
        content: "Other message content",
        embeds: [{
          title: "Other embed",
          // etc
        }]
      }
    }
  ],
  
  // An optional short summary of the message, used for autocompletion.
  // If not specified, a summary will be generated from the message's content and/or embed titles.
  summary: "My message",
  
  // Variants of this message that the querier can select manually, available for tag messages.
  // If specified, the "variant" option in config will be attached to the tag's options.
  // Can be used to select localized versions, change the answer depending on the user's software version, etc.
  // You can make use of HOCON's `include` syntax to create variants in other files.
  variants: {
    spanish: {
      // Message schema with buttons.
    },
  }
}

🤖 Command/Option Schema

The command schema is used when configuring sub/commands and their options.

{
  // The command's name.
  name: "my-command",
  
  // The command's description.
  description: "My command.",
  
  // Optional, localization data for Discord clients on a specific language.
  // See https://discord-api-types.dev/api/0.37.92/discord-api-types-rest/common/enum/Locale#Index for available locales.
  locale: {
    SpanishES: {
      name: "comando",
      description: "Mi comando."
    }
  },
  
  // The command's options. Some commands will have it, some don't.
  // The exact options also depend on the context.
  options: {
    someOption: {
      name: "option",
      description: "This option's description.",
      
      // Optional as well.
      locale: {
        SpanishES: {
          name: "nombre",
          description: "La descripción de esta opción."
        }
      }
    }
  },
  
  // Optional, the command's integration types, overriding the config's defaultIntegrations.
  // In essence, where the command can be installed.
  // See https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationIntegrationType.
  integrationTypes: [
    "GuildInstall", // Will show up when the app is installed on a Guild.
    "UserInstall", // Will show up when the app is installed on a User.
  ],
  
  // Optional, the command's supported interaction contexts, overriding the config's defaultContexts.
  // In essence, where the command can be used after being installed with one of its supported integration types.
  // See https://discord-api-types.dev/api/discord-api-types-v10/enum/InteractionContextType.
  interactionContexts: [
    "BotDM", // Can be used in the bot's DM.
    "PrivateChannel", // Can be used in a private channel including DM Groups, after being "UserInstall"ed.
    "Guild", // Can be used in a guild, after being "GuildInstall"ed.
  ],
}

Caution

Your application must support the integrationTypes you want to use. You can change this on your app's settings, on the Installation page under Installation Contexts.

If you use an integration type that your application doesn't support, an error will be thrown.

🔍 Tag Reference Schema

The tag reference schema is used when "referring" to a tag somewhere and triggering its message. Currently only used for tag buttons.

Caution

Tag references are checked on startup to see if the tag/category/resource exists. If it doesn't, an error will be thrown. This includes referring to a category message when the category doesn't have one configured.

Tip

As a reminder:

  • A tag category and resource IDs are their command names.
  • A tag ID is its first keyword.

Currently available options:

Tag Atlas Tags

Triggering a category message.

{
  category: "my-category"
}

Triggering a tag from a category.

{
  category: "my-category",
  tag: "my-tag"
}

Resource Atlas Tags

Triggering a tag category message inside a resource.

{
  resource: "resource",
  category: "tag-category",
}

Triggering a tag from a resource.

{
  resource: "resource",
  tag: "my-tag"
}

Triggering a tag from a tag category inside a resource.

{
  resource: "resource",
  category: "tag-category",
  tag: "my-tag"
}

⚙ Configuration

The config.conf file lets you configure the behavior or messages of the bot.

{
  // Bot's token
  token: "TOKEN",
  
  // Whether the bot should update its commands when it starts. You can set it to `false` while testing.
  updateCommands: true,

  // Defines where the atlases are.
  source: {
    // Either "local" or "git".
    type: "",

    // The folder where the tag and resource atlases are contained.
    // For "local" type, it's relative to the Enki root (where the package.json file is), and defaults to "content".
    // For "git" type, it's relative to the git repository root, doesn't default to anything, not specifying it loads them directly from root. 
    contentFolder: "content",

    // The following options are only available for the "git" type.

    // The url of the git repository. Must end in `.git`.
    gitUrl: "",

    // Optionally, the folder where repository clones will be stored. Defaults to __clone__.
    cloneFolder: "",
  },

  errors: {
    // Message schema that will be triggered when the user specifies an unknown tag.
    tagNotFound: {
      content: "Unknown tag."
    },

    // Message schema that will be shown to the user for general errors.
    generic: {
      content: "An error has happened, please contact an admin."
    }
  },

  options: {
    // Command option schema to select a variant, for tags that have it.
    variant: {
      name: "variant",
      description: "Select a specific variant of this tag."
    },
    // Command option schema to choose whether the output of the tag should be ephemeral, useful
    // to check what a tag contains before triggering it.
    hide: {
      name: "hide",
      description: "Whether to only show this tag to yourself."
    }
  },
  
  // Default integration types for all commands, unless they explicitly override it.
  // In essence, where the command can be installed.
  // See https://discord-api-types.dev/api/discord-api-types-v10/enum/ApplicationIntegrationType.
  // Optional for backwards compatibility, defaults to ["GuildInstall"]. Will be required in next major version.
  defaultIntegrations: [
    "GuildInstall", // Will show up when the app is installed on a Guild.
    "UserInstall", // Will show up when the app is installed on a User.
  ],
  
  // Default interaction contexts for all commands, unless they explicitly override it.
  // In essence, where the command can be used after being installed with one of its supported integration types.
  // See https://discord-api-types.dev/api/discord-api-types-v10/enum/InteractionContextType.
  // Optional for backwards compatibility, defaults to ["Guild", "BotDM", "PrivateChannel"]. Will be required in next major version.
  defaultContexts: [
    "BotDM", // Can be used in the bot's DM.
    "PrivateChannel", // Can be used in a private channel including DM Groups, after being "UserInstall"ed.
    "Guild", // Can be used in a guild, after being "GuildInstall"ed.
  ],
}

Caution

Your application must support the defaultIntegrationTypes you want to use. You can change this on your app's settings, on the Installation page under Installation Contexts.

If you use an integration type that your application doesn't support, an error will be thrown.

🚀 Running

Enki requires at least Node.js 20.

  1. Run npm install to install the dependencies.
  2. Run npm run build to build the bot.
  3. Fill your config.conf file and fill your atlases.
  4. Run npm run start to start the bot.

Optionally, you can run npm run start:parse to only start the "parsing" part of the bot, which will only parse and assert your content, but not run the bot.