A Discord bot written in Crystal https://amaranth.andrewzah.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Andrew Zah 9b95966dfc
Unescape wiki input
9 months ago
assets Fix bugs regarding rolling restart,add name scheme 11 months ago
spec Update deps, fix reference issues with Guild 9 months ago
src Unescape wiki input 9 months ago
.gitattributes Rouge doesn't support cr, so map cr->ruby syntax 10 months ago
.gitignore Switch from `CrystalBot` to `Amaranth` 11 months ago
.travis.yml Rough implementation of a Plugin handling system 1 year ago
Dockerfile Update github plugin and server for Raze 10 months ago
LICENSE Update LICENSE/shard.yml 9 months ago
Makefile Update github plugin and server for Raze 10 months ago
README.md Switch from `CrystalBot` to `Amaranth` 11 months ago
docker-compose.yml Fix docker-compose bind mounts 11 months ago
shard.lock Update deps, fix reference issues with Guild 9 months ago
shard.yml Update LICENSE/shard.yml 9 months ago

README.md

Amaranth

Amaranth is a Discord bot written Crystal using discordcr and discordcr-middleware.

Anything in src/amaranth could get moved to an external repo at any time.

Functionality

Amaranth now supports plugins and namespaced commands! Available commands are:

Group Commands:

bot: rename
carc: eval, langs
chan: rename
dota: latest
github: add, list, remove
patches: check, disable, enable, games, gids, list, list_all, stop_all
rust: crate?, crates, eval, versions

Regular Commands:

clear, commands, echo, help

So to run a command, use <group>.<command> or <command>.

Writing a Plugin and Commands

Command Creation

Commands have several initialization methods. You must specify a name and desc, but min_args, max_args and permissions are optional.

  • name and desc are of type String.
  • desc shows up when a user types help <command>.
  • permissions are of the type Discord::Permissions.

Example description and output:

desc = <<-DESC
Gets latest dota match.
Usage: dota.latest [dotaID] where ID is optional.
You can save your ID using dota.add. You can find your ID at opendota.com.
DESC

Sample command help.

Initialization Methods

alias ExecType = Proc(Array(String), Discord::Context, CommandReturn)
def initialize(@name, @desc, &@exec : ExecType)
def initialize(@name, @desc, @permissions, &@exec : ExecType)
def initialize(@name, @desc, @min_args : Int32, &@exec : ExecType)
def initialize(@name, @desc, @min_args : Int32, @max_args : Int32, &@exec : ExecType)
def initialize(@name, @desc, @min_args : Int32, @max_args : Int32, @permissions : Discord::Permissions, &@exec : ExecType)
desc = <<-DESC
This command is an example command.
Usage: name
DESC
command "name", desc, do |args, context|
  # etc
end
perms = Discord::Permissions.flags(ReadMessages, SendMessages, KickPeople)
command "example", desc, 1, 2, perms do |args, context|
end

PreCommand Creation

PreCommands are a limited version of Commands:

  • They are not registered (unable to run manually via Discord)
  • They run before every command
  • They currently take a return type of Discord::Message | Nil
  • They do not take args
  • They do not take a description
  • They currently cannot be namespaced.
pre_command do |context|
  # something to be run before any command
end

PreCommands are new to Amaranth so they are prone to change.

Making the plugin

The plugin class needs to inherit from Plugin.

class ExamplePlugin < Plugin::Plugin
end

To make a grouped command that says Hello, world!:

class ExamplePlugin < Plugin::Plugin
  group "example" do
    desc = "My description"
    command "hello", desc do |args, context|
      "Hello, world!"
    end
  end
end

# this registers example.hello as a command!

To make a command without a group, omit group. However, make sure there aren’t multiple commands with the same name.

class ExamplePlugin < Plugin::Plugin
  desc = "My description"
  command "hello", desc do |args, context|
    "Hello, world!"
  end
end

# this registers hello as a command!

Command return types

Amaranth allows you to manually run context.client.create_message() or use implicit returns and automatically handle it for you.

Implicit Return Types:

  • String: Amaranth will run context.client.create_message(context.message.channel.id, <string>)
  • Discord::Embed: Amaranth will run context.client.create_message(context.message.channel.id, "", <embed>)
  • NamedTuple(msg: String, embed: Discord::Embed): Amaranth will run context.client.create_message(context.message.channel.id, <string>, <embed>)
  • Discord::Message: No message will be created.
  • Discord::Channel: No message will be created.
  • Discord::Guild: No message will be created.

Return types like Discord::Channel get returned from context.client commands and thus do not warrant a message.

Note: Returning an empty string (“”) also causes no message to be created.

Saving data in a config

To get access to helper methods for saving a config, you need two things:

  1. Create a config class with a MessagePack mapping.
  2. Extend your plugin class with the config type of your Config.
class ExampleConfig
  MessagePack.mapping({
    ids: Array(UInt64),
    channels: Array(UInt64)
  })
end

class ExamplePlugin < Plugin::Plugin
  extend Plugin::Config(ExampleConfig)
end

Now you get access to two methods:

  1. get_config(name : String) which returns an existing config or creates a new one
  2. save_config(name : String, config : ConfigType) which saves your config
class ExampleConfig
  MessagePack.mapping({
    ids: Array(UInt64),
    channels: Array(UInt64)
  })
end

class ExamplePlugin < Plugin::Plugin
  extend Plugin::Config(ExampleConfig)

  group "example" do
    command "list" do |args, context|
      config = get_config("example")
      config.channels.each do |channel|
        context.client.create_message(channel, "Hello!")
      end
    end
  end
end

Running your Plugin

You’ve created a plugin. Great! Now, how do you run it?

Amaranth runs on discordcr-middleware, so what you’ll want to do is create a stack.

module ExampleBot
  # in src/main.cr, etc
  class_property client = Discord::Client.new(token)
  class_property cache = Discord::Cache.new(client)
  @@client.cache = @@cache

  @@client.stack(:dota,
    Common.new,
    DiscordMiddleware::Error.new("%exception%"),
    DiscordMiddleware::Prefix.new(prefix),
    ParserMW.new(prefix),
    PluginHandler.new(
      [ExamplePlugin],
    ))
end

ExampleBot.client.run

PluginHandler can take an array of an arbitary amount of plugins. Keep in mind to namespace your commands to avoid overwriting issues with commands of the same name.

Recommended reading for help

  • discordcr docs
  • [discordcr-middleware docs]()
  • amaranth middlewares source
    • plugin_handler.cr manages the instantiation and hashing of group and command names, as well as command execution
    • plugin/* defines Plugin, Group, Command, PreCommand, Config in the Plugin namespace
    • parser/* defines Parser::Parser, Parser::Lexer

Roadmap

  • Improve on PreCommands as a concept
  • Improve min_args, max_args checking
  • Implement a help property for each command, so one can run help command to get more information.

Development

Interested in development? Message @ Andrei#8263 (9132965190) on Discord.

Contributing

  1. Fork it ( https://github.com/azah/amaranth/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am ‘Add some feature’)
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

  • azah Andrew Zah - creator, maintainer