Only this pageAll pages
Powered by GitBook
1 of 16

adam

Loading...

First Steps

Loading...

Loading...

Loading...

Loading...

Errors

Loading...

Loading...

Loading...

Localization

Loading...

Loading...

Restrictions

Loading...

Loading...

Getting started

In this guide, you'll learn how to use adam, a discord bot framework written in Go. The aim here is to get you familiar with adam, but not to provide a full reference. Use the documentation for that. If you are unfamiliar with Go, start by taking the Go Tour. With that said, let's get your first bot up and running.

Project Layout

Depending on your use case, a bot's codebase can grow very large. Therefore, it is important to properly organize your bot. We recommend you group your code into three main folders:

cmd: This contains your main package. Here you initialize everything, add your plugins, and start the bot.

plugins: This is where your plugins go. Use one package per command/module.

pkg: This is where all your library code goes. Basically, everything that fits in neither cmd nor plugin.

Note, however, that this layout is not the holy grail of layouts. If you only want to run a small bot with a handful of commands, you might be better of with a custom layout. Still, this is a good start if you are unsure about how to structure your bot.

Example

📁 myBot
 ┣ 📁 cmd
 ┃  ┗ 📁 mybot - your main package running the bot
 ┣ 📁 plugins
 ┃  ┣ 📁 mod - the mod (moderation) module
 ┃  ┃  ┣ 📁 ban - the ban command
 ┃  ┃  ┗ 📁 kick - the kick command
 ┃  ┗ 📁 ping - the ping command
 ┗ 📁 pkg
    ┗ 📁 repository
       ┣ 📁 mongo - mongo db driver
       ┗ 📁 postgres - postgres db driver

Creating a bot

After you have run go mod init and added adam as a dependency using go get github.com/mavolin/adam, you can create a bot in your main package using bot.New. You now need to think about what parts of the bot you want to make configurable, as a lot of configuration is done through the bot.Options you pass to bot.New.

cmd/mybot/main.go
package main

import (
    "log"
    "os"
    "time"

    "github.com/mavolin/adam/pkg/bot"
)

func main() {
    bot, err := bot.New(bot.Options{
        Token:            os.Getenv("DISCORD_BOT_TOKEN"),
        SettingsProvider: bot.NewStaticSettingsProvider(parsePrefixes()...),
        EditAge:          45 * time.Seconds,
     })
     if err != nil {
         log.Fatal(err)
     }
}

func parsePrefixes() []string {
	prefixes := os.Getenv("BOT_PREFIXES")
	if len(prefixes) == 0 {
		return nil
	}

	return strings.Split(prefixes, ",")
}

Adding Plugins

After creating your bot, you can add plugins to it.

cmd/mybot/main.go
package main

import (    
    "github.com/mavolin/myBot/plugin/ping"
    "github.com/mavolin/myBot/plugin/mod"
    
    "github.com/mavolin/adam/pkg/bot"
    "github.com/mavolin/adam/pkg/impl/command/help"
    
    "log"
)

func main() {
    bot, err := bot.New(...)
    if err != nil {
        log.Fatal(err)
    }
    
    addPlugins(bot)
}

func addPlugins(b *bot.Bot) {
    // to add a help command you can either create your own, or use adam's
    // built-in one
    b.AddCommand(help.New(help.Options{}))
}

Head over to the next page, to see how to build your first command.

Error Handling 101

How Error Handling Works

Naturally, as with most worker-type applications, error handling is centralized, meaning not done during the command execution itself, but through a separate error handler. The problem with that is that you often lose important debugging information. Even if you have a fancy logger that shows you file and line, it will be of little use if your error is handled centrally.

Luckily, adam circumvents this problem by attaching stack traces where needed. This means, that you can later utilize this information to trace the error to its source. These stack traces are available through the StackTrace method, which has the same signature as the errors generated by github.com/pkg/errors, meaning it is supported by error handling tools such as sentry (The signature is the same when using reflect, as sentry does. Type assertions on adam's errors that expect github.com/pkg/errors' StackTrace method signature will fail).

Adding Stack Traces

Instead of doing your usual return err, return with return errors.WithStack(err). This will add a stack trace to the error, starting at the line of the return. If you call functions provided by adam, you don't even need to do this as adam already returns errors enriched with stack traces.

When You Don't Need Stack Traces

However, not always does an error mean something went wrong. Sometimes the error is on the user's side and you want to communicate that to them. For cases like those, adam provides different error types apart from the *errors.InternalError generated by errors.WithStack. Head over to the next page to learn more.

Middlewares

Middlewares are functions that are run before a command is invoked. They can be used to inject data into a command's plugin.Context, conditionalize execution, or do things like performance and stats tracking.

Adam itself provides middlewares on both a global and a per-plugin level. Aside from that, you can also add middlewares through the state. The order of execution is as follows.

  1. A message create event or message update event is received by the state's event handler.

  2. All global state middlewares are called.

  3. The MessageCreateMiddlewares or MessageUpdateMiddlewares found in the bot's Options are called.

  4. The router is called, which either routes the command or discards the message if it's not a command.

  5. Adam's global middlewares are called.

  6. The plugin's middlewares are called, starting from the highest-most module to the command itself.

  7. Adam's global post-middlewares are called.

  8. The command is invoked.

Adding middlewares

Global middlewares can be added using bot.Bot.AddMiddleware. Similarly, global post middlewares can be added using bot.Bot.AddPostMiddleware.

To get the plugin middlewares, the bot checks if interface { Middlewares() []bot.MiddlewareFunc } is implemented. Every module.Module already does that, and you can add middlewares using .AddMiddleware(f interface{}). However, commands, being custom types, need to implement that interface manually. You can do so by embedding a bot.MiddlewareManager into your command's struct, which adds the required Middlewares function, as well as an AddMiddleware function to add middlewares to the command.

Supported types

bot.MiddlewareManager, bot.Bot, and module.Module support the following middleware signatures:

  • func(*state.State, interface{})

  • func(*state.State, interface{}) error

  • func(*state.State, *state.Base)

  • func(*state.State, *state.Base) error

  • func(*state.State, *state.MessageCreateEvent)

  • func(*state.State, *state.MessageCreateEvent) error

  • func(*state.State, *state.MessageUpdateEvent)

  • func(*state.State, *state.MessageUpdateEvent) error

  • func(next CommandFunc) CommandFunc

Getting Started

Adam was built with the option of localization in mind. This means everything the user sees in the end is somehow localizable. Before you can use localization, however, you need to set a few things up.

Choosing a Localization Library

Adam does not provide direct support for localization. Instead, it abstracts it using a i18n.Func.

Although abstracted, not all types of i18n libraries are supported. Your localization library must support key-based placeholders, and pluralization through the use of an interface{} that is either a number type or a string containing a number. If you are unsure about which library to use, we recommend Nick Snyder's go-i18n.

Enabling Localization

To enable localization, you need to first use a SettingsProvider that returns a *i18n.Localizer. Localizers are responsible for generating translations. Below is an example using go-i18n.

There are better ways of structuring this outside of package main. Regard this as an MWE.

cmd/mybot/settings_provider.go
package main

import (
    "github.com/diamondburned/ariakwa/v2/discord"
    "github.com/mavolin/adam/pkg/i18n"
    "github.com/mavolin/disstate/v3/pkg/state"
    i18nimpl "github.com/nicksnyder/go-i18n/v2/i18n"
)

type settingsRepository interface {
    GuildSettings(discord.GuildID) (prefixes []string, lang string, err error)
}

func newSettingsProvider(
    repo settingsRepository, bundle *i18nimpl.Bundle,
) bot.SettingsProvider {    
    return func (_ *state.Base, m *discord.Message) (
        []string, *i18n.Localizer, bool,
    ) {    
        prefixes, lang, err := repo.GuildSettings(m.GuildID)
        if err != nil {
            log.Println(err)
            
            // allow execution regardless, and just use fallbacks
            return nil, nil, true
        }
        
        return prefixes, i18n.NewLocalizer(lang, newI18nFunc(bundle, lang)), true
    }
}

func newI18nFunc(b *i18nimpl.Bundle, lang string) i18n.Func {
    l := i18nimpl.NewLocalizer(b, lang)
    
    return func(
        term i18n.Term, placeholders map[string]interface{}, plural interface{},
    ) (string, error) {
        return l.Localize(&i18nimpl.LocalizeConfig{
            MessageID: string(term),
            TemplateData: placeholders,
            PluralCount: plural,
        })
    }
}
cmd/mybot/main.go
package main

import (
    "github.com/mavolin/myBot/pkg/repository/memory"
    
    "github.com/mavolin/adam/pkg/bot"
    "github.com/nicksnyder/go-i18n/v2/i18n"
    "golang.org/x/text/language"
    
    "os"
)

func main() {
    repo := memory.New()
    
    bundle := i18n.NewBundle(language.English)
    bundle.LoadMessageFile("en-US.yaml")

    bot, err := bot.New(bot.Options{
        Token: os.Getenv("DISCORD_BOT_TOKEN"),
        SettingsProvider: newSettingsProvider(repo, bundle),
    })
}

Changing How Errors Are Handled

All built-in errors have a default handler they use when they are returned. However, you might want to change how errors look or behave in certain situations.

Logging

If you want to change how InternalErrors are logged, or if you want to use an error tracker like sentry, you can change the errors.Log function:

import (
    "github.com/getsentry/sentry-go"
    "github.com/mavolin/adam/pkg/errors"
    log "github.com/sirupsen/logrus"
)

func changeLogging() {
    errors.Log = func(err error, ctx *plugin.Context) {
        log.WithFields(log.Fields{
                "err": err,
                "cmd_id": ctx.InvokedCommand.ID,
            }).
            Error("internal error in command")
        
        sentry.CaptureException(err)
    }
}

Embeds

All error handlers that send error messages to the user use the error and info embeds provided by package errors. So to change the looks of errors it's often easier to simply alter the embeds using errors.SetErrorEmbed and errors.SetInfoEmbed instead of changing every error handler.

If you're using localization, make sure to define fallbacks for all text used in the embeds. Otherwise, errors might not get sent, if localization fails.

Type-Specific Handling

If you want to change how certain error types are handled, you can replace the handler function of the error. Every error (except InformationalError) has a handler function titled Handle{{error_name}}. You can use the default handler as a reference.

Individual handling

If you want to change how individual errors are handled, the correct place is the bot's error handler. You can change it by setting a custom one in the bots bot.Options.

If you simply want to ignore errors, you can just filter those out and then call bot.DefaultErrorHandler.

cmd/mybot/main.go
bot, err := bot.New(bot.Options{
    Token: os.Getenv("DISCORD_BOT_TOKEN"),
    ErrorHandler: func(err error, s *state.State, ctx *plugin.Context) {
        if errors.Is(err, bot.ErrUnknownCommand) {
            return
        }
        
        bot.DefaultErrorHandler(err, s, ctx)
    },
})

What Are Restrictions?

Restrictions are a way of controlling who can use your command.

If you ever used other bot frameworks you will have noticed that commands often have fields like NSFW or OwnerOnly to control access to commands. While these are useful in some cases, they are of no use in more complicated scenarios, often resulting in you having to write checks yourself.

Adam solves this through functions. A plugin.RestrictionFunc is a function that takes in a state.State and a *plugin.Context and returns an error. If that error is nil, the restriction is considered passed, if it's not the restriction is considered failed.

You can set restrictions in your command's meta.

How Restrictions Work

Just like you can use && and || to assert bool-type conditions, you can use so-called comparators to assert that restrictions pass. The restriction package provides four comparators, the first two beingrestriction.All and restriction.Any. These, as the name suggests, require all or at least one restriction given to them to pass, respectively. This gives you a more granular control over who can use your command, and who can't.

If a restriction fails, an error message is generated, stating the requirements for using a command. If both restrictions passed to the following restriction.Any fail, an error like the one below will be produced.

import (
    "github.com/diamondburned/arikawa/v2/discord"
    "github.com/mavolin/adam/pkg/impl/restriction"
)

var ensignID discord.RoleID = 1234567890987

restriction.Any(
    restriction.UserPermissions(discord.PermissionAdministrator),
    restriction.All(
        restriction.Roles(ensignID),
        restriction.BotOwner,
    ),
)

Custom Error Messages

While these auto-generated errors are nice in some scenarios, you might want to use custom error messages to better express what is needed from the user. This can be achieved using the other two comparators restriction.Allf and restriction.Anyf. Both take in an additional error parameter that is returned if the restriction fails.

Inside an Allf or Anyf, you do not need to use an Xf comparator again. All and Any will produce the same result.

import (
    "github.com/diamondburned/arikawa/discord"
    "github.com/mavolin/adam/pkg/impl/restriction"
    "github.com/mavolin/adam/pkg/plugin"
)

var ErrRestriction = plugin.NewRestrictionError("You need to be a moderator or " +
    "have the kick permissions to use this command.")

restriction.Anyf(ErrRestriction,
    restriction.Permissions(discord.PermissionManageGuild),
    restriction.Permissions(discord.Kick),
)

Argument Parsing

Every command can define the arguments and flags it requires by setting the Args field in its meta. There you can set which required and optional arguments, as well as what flags your command expects.

Choosing an Argument Parser

The syntax expected from the arguments, i.e. the choice of parser, is deliberately specified separately to allow the easy use of third-party commands. By default adam uses a comma based notation (arg.DelimiterParser), however, commands that require custom notations can opt to use a custom parser by specifying the ArgParser field in the command's meta.

If you don't want to use a comma-based notation for any of your arguments, you can change the global parser in the bot's Options by setting bot.Options.ArgParser.

Adam comes with support for two different notations out of the box: Firstly, arg.DelimiterParser, a delimiter-based parser, which allows you to specify a custom delimiter. Secondly, arg.ShellwordParser, a space-delimited shellword parser. You can find more information about the syntactic requirements of both parsers in their respective docs here and here.

There is also the option to use the RawParser. It does not actually parse and simply hands the input to the sole argument as specified in the command meta's Args field.

Types

Before you can start adding Args to your command, you need to understand how an argument or flag is parsed and validated. Every argument and flag has an arg.Type. Type is an interface, and is basically a miniature parser that parses a single argument. There are a lot of built-in Types, but you can also easily create your own, using said interface. Below is a list of all built-in types, with their Go type equivalents in parentheses:

  • TextChannel (*discord.Channel) – Text or announcement (news) channels.

  • Category (*discord.Channel)

  • VoiceChannel (*discord.Channel)

  • Emoji (*discord.Emoji)

  • RawEmoji (discord.APIEmoji) – The same as Emoji, but not just limited to emojis from the invoking guild.

  • Role (*discord.Role)

  • User (*discord.User)

  • Member (*discord.Member)

  • Switch (bool) – Bool type used exclusively for flags.

  • Integer (int)

  • Decimal (float64)

  • NumericID (uint64)

  • AlphanumericID (string)

  • Text (string)

  • Link (string)

  • RegularExpression (*regexp.Regexp)

  • Code (string) – A Markdown code block.

  • Choice (string) – An enum type.

  • Duration (time.Duration)

  • Time (*time.Time)

  • Date (*time.Time)

  • DateTime (*time.Time)

  • TimeZone (*time.Location)

  • Command (*plugin.RegisteredCommand)

  • Module (*plugin.RegisteredModule)

  • Plugin (nil, *plugin.RegisteredCommand, or *plugin.RegisteredModule)

Examples

mute-Command

To illustrate the usage of an ArgConfig, let's create an ArgConfig for a mute command, that takes in a user, and optionally a reason. Furthermore, the duration of the mute can be specified using a flag.

&arg.Config{
    Required: []arg.RequiredArg{
        {
            Name: "User",
            Type: arg.User,
            Description: "The user that shall be muted.",
        },
    },
    Optional: []arg.OptionalArg{
        {
            Name: "Reason",
            Type: arg.SimpleText,
            Description: "The reason for the mute.",
        },
    },
    Flags: []arg.Flag{
        {
            Name: "duration",
            Aliases: []string{"d"},
            Type: arg.SimpleDuration,
            Description: "The duration to mute the user for.",
        },
    },
}

purge-Command

A purge command is another great example of how ArgConfigs work. In our example, the purge command takes in a required number of messages to delete. The number of messages must be between 1 and 100. Furthermore, we have a user flag, that can be used multiple times to limit the messages to delete to certain users. Lastly, there is also a flag that forces purge to remove pinned messages.

&arg.Config{
    Required: []arg.RequiredArg{
        {
            Name: "Number of Messages",
            Type: &arg.Integer{Min: 1, Max: 100},
            Description: "The number of messages to delete (max. 100).",
        },
    },
    Flags: []arg.Flag{
        {
            Name: "user",
            Aliases: []string{"u"},
            Type: arg.Member,
            Multi: true, // this allows multiple uses of the flag
            Description: "The users to delete messages from.",
        },
        {
            Name: "rm-pins",
            Aliades: []string{"pin", "p"},
            Type: arg.Switch, // Switch is special and can only be used for flags
            Description: "Whether to delete pinned messages as well.",
        },
    },
}

Grouping Commands in Modules

What are Modules?

Apart from commands, adam also provides modules as a way of grouping commands. You might know command groups from other bot frameworks, however, modules are a bit more strict. Each module has its own name and every command in it can only be invoked if prefixed by its module's name.

Creating a Module

Just as commands, a module is also abstracted through the plugin.Module interface. Luckily, using a module is even easier than a command, and one can be created by calling module.New.

plugins/mod/mod.go
package mod

import (
    "github.com/mavolin/adam/pkg/impl/module"
    "github.com/mavolin/adam/pkg/plugin"
)

// New creates a new moderation module.
func New() plugin.Module {
    return module.New(module.Meta{Name: "mod"}})
}

Just like with command.Meta, module.Meta also offers some more fields. Refer to its documentation for more information.

Adding Plugins

Similarly tobot.Bot, module.Module also provides a AddCommand and AddModule method to add plugins.

plugins/mod/mod.go
package mod

import (
    "github.com/mavolin/myBot/plugin/mod/ban"
    "github.com/mavolin/myBot/plugin/mod/kick"

    "github.com/mavolin/adam/pkg/impl/module"
    "github.com/mavolin/adam/pkg/plugin"
)

// New creates a new moderation module.
func New() plugin.Module {
    m := module.New(module.Meta{Name: "mod"}})
    
    m.AddCommand(ban.New())
    m.AddCommand(kick.New())
    
    return m
}

Creating Your First Command

A command is nothing more than a type implementing the plugin.Command interface. That interface is defined as follows:

type Command interface {
    CommandMeta
    Invoke(*state.State, *Context) (interface{}, error)
}

CommandMeta is another interface consisting of getters for the command's metadata. You don't really need to worry about the methods it requires, as the command package provides us with predefined types.

Creating a Command

Using that knowledge, you can create your first command. Create a package with your commands name, as detailed in Project Layout, and add a {{package_name}}.go file to it.

Example: A Ping Command

In the ping package create a ping.go file where you create your command type. To implement the plugin.CommandMeta interface, which is part of the plugin.Command interface, you can embed command.Meta in your Ping type.

plugins/ping/ping.go
package ping

import (
    "github.com/mavolin/adam/pkg/impl/command"
    "github.com/mavolin/adam/pkg/plugin"
)

type Ping struct {
    command.Meta
}

var _ plugin.Command = new(Ping) // compile time check

This leaves your Ping command with only Invoke left, to fully satisfy plugin.Command.

plugins/ping/ping.go
import (
    ...
    "github.com/diamondburned/ariakwa/v2/state"
    "github.com/mavolin/adam/plg/plugin"
    
    "time"
)

...

func (p *Ping) Invoke(_ *state.State, ctx *plugin.Context) (interface{}, error) {
    t := time.Now()
    
    msg, err := ctx.Reply("The ping to discord is `calculating...`")
    if err != nil {
        return nil, err
    }
    
    _, err := ctx.Editf(msg.ID, "The ping to discord is %d ms", 
        time.Since(now).Milliseconds())
    return nil, err
}

Now that we've added Invoke, let's add a constructor function where we create a new Ping instance and fill the command.Meta struct we embedded earlier.

plugins/ping/ping.go
import (
    ...
    "github.com/diamondburned/arikawa/v2/discord"
    "github.com/mavolin/adam/pkg/plugin"
    "github.com/mavolin/adam/pkg/impl/command"
)

var _ plugin.Command = new(Ping) // compile-time check

// New creates a new Ping command.
func New() *Ping {
    return &Ping{
        Meta: command.Meta{
            Name:             "ping",
            ShortDescription: "Tells you the ping to Discord's servers.",
            ChannelTypes:     plugin.AllChannels,
            BotPermissions:   discord.SendMessagesPermission,
        },
    }
}
    

func (p *Ping) Invoke(_ *state.State, ctx *plugin.Context) (interface{}, error) {
    ...
}

command.Meta has a lot more fields than just those we used for the Ping command. Refer to its documentation for more information.

Invoke's Return Values

As you might have noticed, plugin.Command.Invoke has two return values, and while the error return value might be a bit more self-explanatory, interface{} is probably not. So what can you use it for?

Everything you return as the first value will get turned into a message and sent in the invoking channel back to the user using the plugin.Context's Replier. Obviously, not everything can be turned into a message, at least not without causing confusion. Therefore, only the following return types are allowed:

  • uint, uint8, uint16, uint32, uint64

  • int, int8, int16, int32, int64

  • float32, float64

  • string

  • discord.Embed and *discord.Embed

  • *embedutil.Builder

  • api.SendMessageData

  • i18n.Term

  • *i18n.Config

  • any type implementingplugin.Reply

Of course, justnil is also valid, and won't create any response.

The second return value, error, is handed to the bot's error handler if it's not nil. While you can define a custom error handler, the default one will wrap every error not implementing errors.Error into a errors.InternalError and call Handle on it. We'll take a closer look at this later on in the Error Types chapter.

Creating Custom Restrictions

Package restriction provides some defaults, but you can also create your own. Before you do that there are a few things to keep in mind.

What Errors to Return

The plugin package provides the RestrictionError type. intended to be used by restrictions. As mentioned in Error Types, there are two types of RestrictionErrors, fatal and non-fatal ones.

A fatal RestrictionError should be returned, if the user has no way of influencing the occurrence of the error by themselves. Simply ask yourself, "If this restriction fails, should the help command still list this command for the user?". If so, use a non-fatal RestrictionError.

If an internal error occurs during the execution of a restriction, it of course should be returned as-is.

Performing Time-Consuming Tasks

Restrictions should never perform time-consuming tasks, without ensuring that the results are cached. This is simply because help commands may check the restrictions of every command to determine whether the user may access them.

Therefore, it is recommended to either perform expensive tasks once and store the result using plugin.Context.Set, or access this kind of data through a cache-layer to prevent reoccurring delays.

The getters getting Discord data found in the plugin.Context are cached for the duration of the command's execution regardless of the state's cache settings, and are therefore safe to access.

How to Localize

The *i18n.Localizer returned by the bot.SettingsProvider we created on the previous page is now available in the *plugin.Context.

Using the Localizer

Localized text is generated using *i18n.Configs. Usually, these are placed in a terms.go file in your package. If you have many configs, it might also be advisable to group terms into foo_terms.go, bar_terms.go etc.

In your command's Invoke method, you can then use those *i18n.Configs to generate messages.

plugins/sum/terms.go
package sum

import "github.com/mavolin/adam/pkg/i18n"

var (
    shortDescription = i18n.NewFallbackConfig(
        "plugin.sum.short_description", "Add numbers together")
    
    argsNumbersName = i18n.NewFallbackConfig(
        "plugin.sum.args.numbers.name", "Numbers")
    argsNumbersDescription = i18n.NewFallbackConfig(
        "plugin.sum.arg.numbers.description", 
        "The numbers that shall be added.")
)

var (
    errorNotEnoughNumbers = i18n.NewFallbackConfig(
        "plugin.sum.error.not_enough_numbers", // term
        "I need at least 2 numbers to calculate a sum!") // fallback
    
    result = i18n.NewFallbackConfig(
        "plugin.sum.result", "The sum is {{.sum}}.")
)

// The recommended way to fill placeholders is by using data structs.
// All exported fields will by put into snake_case and given to the Localizer
// as placeholders.
// If you want to use custom names, use the `i18n:"my_custom_name"` struct tag.

type resultPlaceholders struct {
    Sum int
}
plugins/sum/sum.go
package sum

import (
    "github.com/mavolin/adam/pkg/i18n"
    "github.com/mavolin/adam/pkg/impl/arg"
)

type Sum struct {
    command.Meta
}

func New() *Sum {
    return &Sum{
        Meta: command.LocalizedMeta{
            Name: "sum",
            ShortDescription: shortDescription,
            Args: &arg.LocalizedConfig{
                RequiredArgs: []arg.LocalizedRequiredArg{
                    {
                        Name: argsNumbersName,
                        Type: arg.Integer,
                        Description: argsNumbersDescription,
                    },
                },
                Variadic: true,
            },
        },
    }
}

func (s *Say) (_ *state.State, ctx *plugin.Context) (interface{}, error) {
    nums := ctx.Args.Ints(0)
    if len(nums) >= 1 {
        return nil, errors.NewUserErrorl(errorNotEnoughNumbers)
    }
    
    var sum int
    
    for _, num := range nums {
        sum += num
    }
    
    return result.WithPlaceholders(resultPlaceholders{
        Sum: sum,
    }, nil
}

Working with Localizers

As you might have already noticed, there are a lot of utilities for working with localized text. Structs typically come in two versions X and LocalizedX to provide localization support. If they are constructor-based, they might also use constructors like NewX, NewXl, and NewXlt where NewX creates an unlocalized version, NexXl uses a *i18n.Config, and NewXlt uses a i18n.Term.

The same also applies to other functions. For example plugin.Context provides Reply, Replyl and Replylt.

Error Types

As mentioned before, adam uses multiple error types that behave differently when handled. Below is a list of all built-in errors.Error, the interface used to abstract errors generated by adam.

You may also encounter "normal" errors, that don't implement errors.Error, such as msgawait.TimeoutError. Those usually implement the interface used by errors.As to make themselves available as an errors.Error. Don't worry about this too much, the default error handler already takes care of the conversion for you.

errors.InformationalError

This error does nothing when handled and simply serves as a signaling error much like io.EOF.

There is also a default, errors.Abort, that can be used to signal a user-triggered stop of execution.

Constructors

  • errors.NewInformationalError(desc string)

errors.InternalError

This is the most common error and is also created when using errors.WithStack. Everything that goes wrong during the execution and is not the user's fault, is considered an InternalError. By default they are logged.

InternalErrors can be divided into two categories:

Silent InternalErrors are errors that are logged via errors.Log, but their occurrence is not communicated to the user through a message. This is useful if a function can safely continue even though an error occurred, but the error shall still be captured.

Non-Silent InternalErrors are errors that are logged via errors.Log and send a message in the invoking channel. All constructors below that don't contain the word Silent produce such non-silent errors. By default a non-silent InternalError sends a generalized message, however, it can be customized using errors.WithDescription and it's derivatives.

You can also convert a silent error into a non-silent error and vice versa:

nonSilent      := errors.NewWithStack("something broke")
silent         := errors.Silent(nonSilentError)
nonSilentAgain := errors.WithStack(silent)

In the above example a new non-silent InternalError is created, converted into a silent InternalError, and converted back again.

Furthermore, all silent constructors discard non-InternalErrors, i.e. they return nil. This is done to prevent user-related errors, i.e. any error other than an InternalError, from sending messages as to not break the requested "silence". However, this rule does not apply to errors.MustSilent. Refer to it's doc for more information.

Constructors

  • errors.NewWithStack(text string)

  • errors.NewSilent(text string)

  • errors.NewWithStackf(format string, a ...interface{})

  • errors.NewSilentf(format string, a ...interface{})

  • errors.WithStack(err error)

  • errors.Silent(err error)

  • errors.MustInternal(err error)

  • errors.MustSilent(err error)

  • errors.Wrap(err error, message string)

  • errors.WrapSilent(err error, message string)

  • errors.Wrapf(err error, format string, a ...interface{})

  • errors.WrapSilentf(err error, format string, a ...interface{})

  • errors.WithDescription(err error, description string)

  • errors.WithDescriptionf(err error, format string, a interface{}...)

  • errors.WithDescriptionl(err error, description *i18n.Config)

  • errors.WithDescriptionlt(err error, description i18n.Term)

errors.UserError

A UserError is the type used to signal the user they did something wrong, and isn't logged.

It is basically an *embedutil.Builder, but uses a template embed to unify its style. You can customize the base embed using errors.SetErrorEmbed.

Defaults

  • bot.ErrUnknownCommand

Constructors

  • errors.NewCustomUserError()

  • errors.NewUserErrorFromEmbed(e *embedutil.Builder)

  • errors.NewUserError(description string)

  • errors.NewUserErrorl(description *i18n.Config)

  • errors.NewUserErrorlt(description i18n.Term)

errors.UserInfo

A UserInfo is less critical UserError. By default, its embed color is not red like its error counterpart but blue.

Constructors

  • errors.NewCustomUserInfo()

  • errors.NewUserInfoFromEmbed(e *embedutil.Builder)

  • errors.NewUserInfo(description string)

  • errors.NewUserInfol(description *i18n.Config)

  • errors.NewUserInfolt(description i18n.Term)

plugin.ArgumentError

This error gets returned by plugin.ArgConfig if something is wrong with the arguments or flags supplied by the user. No logging here either.

Constructors

  • plugin.NewArgumentError(description string)

  • plugin.NewArguementErrorl(description *i18n.Config)

  • plugin.NewArgumentErrorlt(description i18n.Term)

plugin.BotPermissionsError

This error gets returned every time the bot needs permissions to do something but hasn't. If that's the case, the user will be supplied a list of permissions they need to grant the bot in order to proceed.

Defaults

  • plugin.DefaultBotPermissionsError

Constructors

  • plugin.NewBotPermissionsError(missing discord.Permissions)

plugin.ChannelTypeError

A plugin.ChannelTypeError is used if a command is invoked in a channel with a channel type not allowed by the command. It lists the channel types allowed to use.

Constructors

  • plugin.NewChannelTypeError(allowed plugin.ChannelType)

plugin.RestrictionError

If a plugin.RestrictionFunc fails, it typically returns this error. There are fatal and non-fatal plugin.RestrictionErrors, as marked by their .Fatal field. This is important for help commands to decide whether they should show the command in the help text: When listing all commands the help command checks each command whether it's restricted. If a command returns a fatal restriction, it is excluded from the list.

Defaults

  • plugin.DefaultRestrictionError

  • plugin.DefaultFatalRestrictionError

Constructors

  • plugin.NewRestrictionError(description string)

  • plugin.NewRestrictionErrorl(description *i18n.Config)

  • plugin.NewRestrictionErrorlt(description i18n.Term)

  • plugin.NewFatalRestrictionError(description string)

  • plugin.NewFatalRestrictionErrorl(description *i18n.Config)

  • plugin.NewFatalRestrictionErrorlt(description i18n.Term)

plugin.ThrottlingError

This error is returned by types implementing plugin.Throttler and indicates that the user is being throttled.

Constructors

  • plugin.NewThrottlingError(description string)

  • plugin.NewThrottlingErrorl(description *i18n.Config)

  • plugin.NewThrottlingErrorlt(description i18n.Term)