Expand description
Poise is an opinionated Discord bot framework with a few distinctive features:
- edit tracking: when user edits their message, automatically update bot response
- slash commands: completely define both normal and slash commands with a single function
- flexible argument parsing: command parameters are defined with normal Rust types and parsed automatically
§Quickstart
use poise::serenity_prelude as serenity;
struct Data {} // User data, which is stored and accessible in all command invocations
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
/// Displays your or another user's account creation date
#[poise::command(slash_command, prefix_command)]
async fn age(
ctx: Context<'_>,
#[description = "Selected user"] user: Option<serenity::User>,
) -> Result<(), Error> {
let u = user.as_ref().unwrap_or_else(|| ctx.author());
let response = format!("{}'s account was created at {}", u.name, u.created_at());
ctx.say(response).await?;
Ok(())
}
#[tokio::main]
async fn main() {
let token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN");
let intents = serenity::GatewayIntents::non_privileged();
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands: vec![age()],
..Default::default()
})
.setup(|ctx, _ready, framework| {
Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data {})
})
})
.build();
let client = serenity::ClientBuilder::new(token, intents)
.framework(framework)
.await;
client.unwrap().start().await.unwrap();
}
To run commands, ping your bot and write the command name and arguments after. Run the register command to register slash commands, after which you can use those, too.
See examples/feature_showcase/ in the git repository for a full-featured example bot, showcasing most
features of poise: cargo run --example=feature_showcase
§Introduction to serenity
Serenity is the Discord API wrapper library poise is built on top of. Using poise automatically means using serenity, so here’s a couple tips:
§impl Trait
parameters
Many serenity functions take an argument of type impl CacheHttp
or
impl AsRef<Http>
. You can pass in any type that implements these traits, like
crate::Context
or serenity::Context
.
§Gateway intents
To run a Discord bot, you need to set gateway intents: a list of event types you want to receive
from Discord. A sensible default is serenity::GatewayIntents::non_privileged()
which contains
all event types except privileged ones. Privileged intents require manual enabling in your bot
dashboard to use (and large bots require whitelisting by Discord). A notable privileged intent
is MESSAGE_CONTENT which is required for poise prefix
commands.
To set multiple gateway events, use the OR operator:
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT
§Discord actions outside a command
You can run Discord actions outside of commands by cloning and storing serenity::CacheHttp
/
Arc<serenity::Http>
/Arc<serenity::Cache>
. You can get
those either from serenity::Context
(passed to
setup
and all commands via
ctx.serenity_framework()
) or before starting the client via
http
and cache
.
Pass your CacheHttp
or Arc<Http>
to serenity functions in place of the usual
serenity::Context
§Useful serenity methods
Many serenity structs have an ID field. Some useful methods are defined only on the Id types. For example:
serenity::Guild
andserenity::GuildId
serenity::User
andserenity::UserId
serenity::Role
andserenity::RoleId
- …
Some examples for methods you should have in your repertoire:
serenity::GuildId::to_guild_cached
andserenity::GuildId::to_partial_guild
to access full guild dataserenity::ChannelId::to_channel
to access full channel dataserenity::Channel::guild
to try convert a genericserenity::Channel
into aserenity::GuildChannel
, to access guild specific channel data
§Introduction to slash commands
Discord slash commands can be a bit unintuitive at first. If you’re unfamiliar, please read this
To activate a slash command, your bot
needs to register in on Discord. You may want to do this manually, with a register
command
(poise provides builtins::register_application_commands_buttons
as a starting point for that), or you
may want to re-register commands automatically on every bot startup. Choose what you prefer. Also
see Registering Slash Commands.
Commands can be registered globally or per guild. Global commands are available on every guild your bot is invited on, but it takes up to an hour for global registration to roll out. Per guild registration only updates a single guild, but it happens instantly, which is useful for testing.
Your bot also needs to be invited with the applications.commands
scope. For example, in Discord’s
invite link generator (discord.com/developers/applications/XXX/oauth2/url-generator),
tick the applications.commands
box.
§How to use
§Create commands
Every command is represented by a function annotated with #[poise::command]
:
/// Description of the command here
///
/// Here you can explain how the command \
/// is used and how it works.
#[poise::command(prefix_command, /* add more optional command settings here, like slash_command */)]
async fn command_name(
ctx: Context<'_>,
#[description = "Description of arg1 here"] arg1: serenity::Member,
#[description = "Description of arg2 here"] arg2: Option<u32>,
) -> Result<(), Error> {
// Command code here
Ok(())
}
See #[poise::command]
for detailed information.
§Subcommands
Commands in poise have a tree structure. Every commands refers to a list of subcommands, which you
can easily set using the command
macro like so:
#[poise::command(prefix_command, slash_command, subcommands("child1", "child2"))]
pub async fn parent(ctx: Context<'_>, arg: String) -> Result<(), Error> { Ok(()) }
#[poise::command(prefix_command, slash_command)]
pub async fn child1(ctx: Context<'_>, arg: String) -> Result<(), Error> { Ok(()) }
#[poise::command(prefix_command, slash_command)]
pub async fn child2(ctx: Context<'_>, arg: String) -> Result<(), Error> { Ok(()) }
With this setup, users can call ~parent [arg]
or ~parent child1 [arg]
or ~parent child2 [arg]
.
Slash command subcommands are also supported, but the base command (/parent
) cannot be used
as per Discord; only the leaf commands (/parent child1 [arg]
, /parent child2 [arg]
).
When adding the commands to the framework, add just the parent command (since it fully contains its subcommands):
let options = poise::FrameworkOptions {
commands: vec![
parent(),
],
..Default::default()
};
Subcommands are stored in Command::subcommands
. As with all Command
fields, you can
programmatically modify those any way you’d like. The command
macro is just a convenience thing
to set the fields for you.
For another example of subcommands, see examples/feature_showcase/subcommands.rs
.
§Big example to showcase many command features
Also see the command
macro docs
use poise::serenity_prelude as serenity;
type Data = ();
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
/// A test command for poise
#[poise::command(
prefix_command,
track_edits,
hide_in_help,
required_permissions = "SEND_MESSAGES",
aliases("bigounce", "abomination"),
help_text_fn = "my_huge_ass_command_help",
check = "check",
on_error = "error_handler",
)]
async fn my_huge_ass_command(
ctx: Context<'_>,
#[description = "Consectetur"] ip_addr: std::net::IpAddr, // implements FromStr
#[description = "Amet"] user: serenity::Member, // implements ArgumentConvert
#[description = "Sit"] code_block: poise::CodeBlock, // implements PopArgument
#[description = "Dolor"] #[flag] my_flag: bool,
#[description = "Ipsum"] #[lazy] always_none: Option<String>,
#[description = "Lorem"] #[rest] rest: String,
) -> Result<(), Error> {
Ok(())
}
fn my_huge_ass_command_help() -> String {
String::from("\
Example usage:
~my_huge_ass_command 127.0.0.1 @kangalio `i = i + 1` my_flag rest of the message")
}
async fn check(ctx: Context<'_>) -> Result<bool, Error> {
// We discriminate against users starting with an X
Ok(!ctx.author().name.starts_with('X'))
}
async fn error_handler(error: poise::FrameworkError<'_, Data, Error>) {
println!("Oh noes, we got an error: {:?}", error);
}
§Create and configure framework
use poise::serenity_prelude as serenity;
// Use `Framework::builder()` to create a framework builder and supply basic data to the framework:
let framework = poise::Framework::builder()
.setup(|_, _, _| Box::pin(async move {
// construct user data here (invoked when bot connects to Discord)
Ok(())
}))
// Most configuration is done via the `FrameworkOptions` struct, which you can define with
// a struct literal (hint: use `..Default::default()` to fill uninitialized
// settings with their default value):
.options(poise::FrameworkOptions {
on_error: |err| Box::pin(my_error_function(err)),
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some("~".into()),
edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan(std::time::Duration::from_secs(3600)))),
case_insensitive_commands: true,
..Default::default()
},
// This is also where commands go
commands: vec![
command1(),
command2(),
// You can also modify a command by changing the fields of its Command instance
poise::Command {
// [override fields here]
..command3()
}
],
..Default::default()
}).build();
let client = serenity::ClientBuilder::new("...", serenity::GatewayIntents::non_privileged())
.framework(framework).await;
client.unwrap().start().await.unwrap();
§Registering slash commands
As explained in Introduction to slash commands, slash
commands need to be registered to Discord. Poise provides several ways to do it, with varying
degree of abstraction. (Note: you can access a list of framework commands from anywhere with
ctx.framework().options.commands
).
The easiest way is with builtins::register_application_commands_buttons
.
It spawns a message with buttons to register and unregister all commands, globally or in the current
guild (see its docs).
A more flexible approach is to serialize the commands to a Vec<serenity::CreateCommand>
using builtins::create_application_commands
. That way, you can call serenity’s registration
functions manually:
For example, you could call this function in FrameworkBuilder::setup
to automatically
register commands on startup. Also see the docs of builtins::create_application_commands
.
The lowest level of abstraction for registering commands is Command::create_as_slash_command
and Command::create_as_context_menu_command
.
§Tips and tricks
§Type aliases
As seen in the examples, it’s useful to define type aliases for Context
with
your bot’s error type and user data type filled in:
type Context<'a> = poise::Context<'a, UserData, ErrorType>;
§Serenity prelude
When you’re too lazy to import serenity items from their full path which can be quite lengthy at
times, you can use poise::serenity_prelude
: a module which reexports almost all items from
serenity.
use poise::serenity_prelude as serenity;
// Short paths!
serenity::Member, serenity::UserId, serenity::ReactionType, serenity::GatewayIntents
§Unit testing
Unit testing a Discord bot is difficult, because mocking the Discord API is an uphill battle. Your best bet for unit testing a Discord bot is to extract the “business logic” into a separate function - the part of your commands that doesn’t call serenity functions - and unit test that.
Example:
#[poise::command(slash_command)]
pub async fn calc(ctx: Context<'_>, expr: String) -> Result<(), Error> {
let ops: &[(char, fn(f64, f64) -> f64)] = &[
('+', |a, b| a + b), ('-', |a, b| a - b), ('*', |a, b| a * b), ('/', |a, b| a / b)
];
for &(operator, operator_fn) in ops {
if let Some((a, b)) = expr.split_once(operator) {
let result: f64 = (operator_fn)(a.trim().parse()?, b.trim().parse()?);
ctx.say(format!("Result: {}", result)).await?;
return Ok(());
}
}
ctx.say("No valid operator found in expression!").await?;
Ok(())
}
Can be transformed into
fn calc_inner(expr: &str) -> Option<f64> {
let ops: &[(char, fn(f64, f64) -> f64)] = &[
('+', |a, b| a + b), ('-', |a, b| a - b), ('*', |a, b| a * b), ('/', |a, b| a / b)
];
for &(operator, operator_fn) in ops {
if let Some((a, b)) = expr.split_once(operator) {
let result: f64 = (operator_fn)(a.trim().parse().ok()?, b.trim().parse().ok()?);
return Some(result);
}
}
None
}
#[poise::command(slash_command)]
pub async fn calc(ctx: Context<'_>, expr: String) -> Result<(), Error> {
match calc_inner(&expr) {
Some(result) => ctx.say(format!("Result: {}", result)).await?,
None => ctx.say("Failed to evaluate expression!").await?,
};
Ok(())
}
// Now we can test the function!!!
#[test]
fn test_calc() {
assert_eq!(calc_inner("4 + 5"), Some(9.0));
assert_eq!(calc_inner("4 / 5"), Some(0.2));
assert_eq!(calc_inner("4 ^ 5"), None);
}
§About the weird name
I’m bad at names. Google lists “poise” as a synonym to “serenity” which is the Discord library underlying this framework, so that’s what I chose.
Also, poise is a stat in Dark Souls
Re-exports§
pub use choice_parameter::*;
pub use cooldown::*;
pub use dispatch::*;
pub use framework::*;
pub use macros::*;
pub use modal::*;
pub use prefix_argument::*;
pub use reply::*;
pub use slash_argument::*;
pub use structs::*;
pub use track_edits::*;
Modules§
- builtins
- Building blocks for common commands like help commands or application command registration
- choice_
parameter - Contains the
ChoiceParameter
trait and the blanketcrate::SlashArgument
andcrate::PopArgument
impl - cooldown
- Infrastructure for command cooldowns
- dispatch
- Contains all code to dispatch incoming events onto framework commands
- framework
- The central Framework struct that ties everything together.
- macros
- Procedural macros used in poise, like
command
- modal
- Modal trait and utility items for implementing it (mainly for the derive macro)
- prefix_
argument - Everything related to parsing command arguments from a text message
- reply
- Infrastructure for replying, i.e. sending a message in a command context
- samples
Deprecated - See
builtins
- serenity_
prelude - This module re-exports a bunch of items from all over serenity. Useful if you can’t remember the full paths of serenity items.
- slash_
argument - Application command argument handling code
- structs
- Plain data structs that define the framework configuration.
- track_
edits - Tools for implementing automatic edit tracking, i.e. the bot automatically updating its response when the user edits their command invocation message.
Macros§
- create_
slash_ argument - Full version of
crate::SlashArgument::create
. - extract_
slash_ argument - Full version of
crate::SlashArgument::extract
. - parse_
prefix_ args - Macro for parsing an argument string into multiple parameter types.
- parse_
slash_ args - Macro for extracting and parsing slash command arguments out of an array of
serenity::CommandDataOption
. - pop_
prefix_ argument - Full version of
crate::PopArgument::pop_from
. - slash_
argument_ choices - Full version of
crate::SlashArgument::choices
.
Type Aliases§
- BoxFuture
- Shorthand for a wrapped async future with a lifetime, used by many parts of this framework.