Kenny Jue

home  about
      

sayori

February 03, 2020

Project Cover by u/DigitalSolstice

About This Project

Sayori is a dead simple command router based on discordgo with

  • custom guild prefix support
  • message parsing
  • multiple command aliases
  • subcommands
  • middlewares.

It’s currently in v2, with v1 being no longer maintained.

I originally developed Sayori to assist in another Discord Bot project. Due to its complexity, I quickly realized discordgo - which only provides low level bindings to the Discord API - would become difficult to maintain in the future. I had some problems I wanted to solve easily within my project architecture:

  • Can I reduce the boilerplate needed to define a command?
  • How can I effectively define all my commands in a single package?
  • What if I want to implement subcommands to my top-level commands? With only discordgo, this is only possible by nesting handlers in a switch/if block and executing them if a command alias matches - hardly an ideal solution.
  • Can I create a standardized invocation context for each applicable message (containing what command was executed, the arguments used, the message itself, etc.)

Of course, this sentiment I had was hardly unique. The packages rfrouter and dgrouter were created with this use case in mind. However, as rfrouter was reflection based and dgrouter generously uses closures (thus greatly increasing code complexity, in my opinion), I decided to implement my own.

How It Works

As an example, assume the following is a Discord message which invokes the echo command (which has no subcommands):

!echo fmt jolly cow leaps over the moon!

  • In this situation, the guild prefix is ! which denotes that the following content will be a command invocation.
  • echo is a command alias for the echo command.
  • fmt jolly cow leaps over the moon! are the arguments for the echo command.

But what if the echo command contains a subcommand called fmt? Then, assuming the same example, this will now be true:

  • echo is a command alias for the echo command.
  • fmt is the command alias for the fmt subcommand.
  • jolly cow leaps over the moon! are the arguments for the fmt subcommand.

Observe that the arguments jolly cow leaps over the moon! get “routed” to the deepest applicable subcommand handler for processing. Sayori abstracts this pipeline, reducing code complexity when developing the bot itself. Sayori will also identify the command trace. In this example, it is ordered as ["echo", "fmt"].

To create an echo command with Sayori, all you need is to build a command with a custom type which implements the Commander interface. Below is a demonstration of initializing the example above with an echo command and a fmt subcommand, where Prefix is an implementation of the Prefixer interface to identify the ! symbol:

router := sayori.New(dg)

echoFmt := sayori.NewRoute(nil).Do(&EchoFmt{}).On("fmt")
echo := sayori.NewRoute(&Prefix{}).Do(&Echo{}).On("echo").Has(echoFmt)

router.Has(echo)

On top of this, Sayori is unopinionated and also supports the default discordgo method of adding command handlers.

Installation

You can install the latest release of Sayori by using:

go get github.com/pixeltopic/sayori/v2

Then include Sayori in your application:

import sayori "github.com/pixeltopic/sayori/v2"

Usage

package main

import (
	"github.com/bwmarrin/discordgo"
	sayori "github.com/pixeltopic/sayori/v2"
)

func main() {
    dg, err := discordgo.New("Bot " + "<mydiscordtoken>")
    if err != nil {
        return
    }
    
    router := sayori.New(dg)
    
    echo := sayori.NewRoute(&Prefix{}).Do(&Echo{}).On("echo", "e")
    
    router.Has(echo)
    
    err = router.Open()
    if err != nil {
        return
    }
}

See /examples for detailed usage.

Links