Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
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.
If you want to change how InternalError
s are logged, or if you want to use an error tracker like sentry, you can change the errors.Log
function:
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.
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.
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
.
Loading...
Loading...
Loading...
Loading...
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.
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
.
Just like with command.Meta
, module.Meta
also offers some more fields. Refer to its documentation for more information.
Similarly tobot.Bot
, module.Module
also provides a AddCommand
and AddModule
method to add plugins.
A command is nothing more than a type implementing the plugin.Command
interface. That interface is defined as follows:
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.
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.
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.
This leaves your Ping
command with only Invoke
left, to fully satisfy plugin.Command
.
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.
command.Meta
has a lot more fields than just those we used for the Ping
command. Refer to its documentation for more information.
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.
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.
A message create event or message update event is received by the state's event handler.
All global state middlewares are called.
The MessageCreateMiddlewares
or MessageUpdateMiddlewares
found in the bot's Option
s are called.
The router is called, which either routes the command or discards the message if it's not a command.
Adam's global middlewares are called.
The plugin's middlewares are called, starting from the highest-most module to the command itself.
Adam's global post-middlewares are called.
The command is invoked.
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.
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
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.
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.
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
.
After creating your bot, you can add plugins to it.
Head over to the next page, to see how to build your first command.
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).
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.
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.
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" error
s, 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.
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.
InternalError
s can be divided into two categories:
Silent InternalError
s 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 InternalError
s 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:
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.
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
.
bot.ErrUnknownCommand
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.
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.
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.
plugin.DefaultBotPermissionsError
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.
plugin.NewChannelTypeError(allowed plugin.ChannelType)
plugin.RestrictionError
If a plugin.RestrictionFunc
fails, it typically returns this error. There are fatal and non-fatal plugin.RestrictionError
s, 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.
plugin.DefaultRestrictionError
plugin.DefaultFatalRestrictionError
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.
plugin.NewThrottlingError(description string)
plugin.NewThrottlingErrorl(description *i18n.Config)
plugin.NewThrottlingErrorlt(description i18n.Term)
The *i18n.Localizer
returned by the bot.SettingsProvider
we created on the previous page is now available in the *plugin.Context
.
Localizer
Localized text is generated using *i18n.Config
s. 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.
Localizer
sAs 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
.
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.
Adam does not provide direct support for localization. Instead, it abstracts it using a i18n.Func
.
To enable localization, you need to first use a SettingsProvider
that returns a *i18n.Localizer
. Localizer
s are responsible for generating translations. Below is an example using go-i18n.
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.
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
.
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 Type
s, 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
)
mute
-CommandTo 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.
purge
-CommandA purge
command is another great example of how ArgConfig
s 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.
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 .
There are better ways of structuring this outside of package main
. Regard this as an .
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 and .
There is also the option to use the . It does not actually parse and simply hands the input to the sole argument as specified in the command meta's Args
field.
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.
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.
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.
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.
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.
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.
The plugin
package provides the RestrictionError
type. intended to be used by restrictions. As mentioned in , there are two types of RestrictionError
s, fatal and non-fatal ones.