Writing an example bot

Create a new project:

cargo new --bin mybot

Add robespierre, robespierre-cache, robespierre-http, robespierre-events, and robespierre-models:

[dependencies]
robespierre = { version = "0.3.0", features = ["cache", "events", "framework", "framework-macros"] }
robespierre-cache = "0.3.0"
robespierre-http = "0.3.0"
robespierre-events = "0.3.0"
robespierre-models = "0.3.0"

We'll also need tokio, and if you want logging, tracing:

tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.2" # not necessarely this but some global subscriber is required for logging
// src/main.rs
fn main() {
    println!("Hello, world!");
}

Let's start with a stub main, running on tokio:

// src/main.rs
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    Ok(())
}

Optional: initialize a global tracing subscriber:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    Ok(())
}

Now, get the bot token:

// src/main.rs
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();
 
    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    Ok(())
}

Create an authentication:

// src/main.rs
use robespierre::Authentication;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    // --snip--
    Ok(())
}

Then a http client:

// src/main.rs
use robespierre::Authentication;
use robespierre_http::Http;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;

    // --snip--
    Ok(())
}

And a websocket connection:

// src/main.rs
use robespierre::Authentication;
use robespierre_events::Connection;
use robespierre_http::Http;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;

    let connection = Connection::connect(&auth).await?;

    // --snip--
    Ok(())
}

Now let's write a basic event handler (similar to how they work in serenity):

// src/main.rs
use robespierre::Context;
use robespierre::Authentication;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::events::ReadyEvent;

// --snip--
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;

    let connection = Connection::connect(&auth).await?;

    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {
    async fn on_ready(&self, ctx: Context, ready: ReadyEvent) {
        tracing::info!("We're ready!");
    }
}

And create the context and handler:

// src/main.rs
use robespierre::Context;
use robespierre::Authentication;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::events::ReadyEvent;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;

    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()); // (*)
    // and if you want a cache:
    let context = context.with_cache(CacheConfig::default());
    let handler = Handler;

    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {
    async fn on_ready(&self, ctx: Context, ready: ReadyEvent) {
        tracing::info!("We're ready!");
    }
}

*) More on this in the user data chapter, for now just leave it as it is.

Now let's "run" the connection, listening to events and handling them:

// src/main.rs
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::events::ReadyEvent;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;

    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let handler = Handler;

    let handler = EventHandlerWrap::new(handler); // explained in a bit
    connection.run(context, handler).await?;

    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {
    async fn on_ready(&self, ctx: Context, ready: ReadyEvent) {
        tracing::info!("We're ready!");
    }
}

First, we have to make a distinction between the 2 types of handlers availabe:

  • T: EventHandler
  • T: RawEventHandler

The EventHandler contains functions like on_ready, on_message, on_message_update, while the RawEventHandler only contains the function async fn handle(self, context, event), which is supposed to handle any type of event the server sends.

Now, Connection::run takes a RawEventHandler, but our Handler is an EventHandler, not a RawEventHandler. The EventHandlerWrap is an utility to convert the events from a RawEventHandler to call the functions of an EventHandler. It itself is a RawEventHandler.

Now, since we used a cache in the context, we also have to update it when we receive events. That can be done by using another utility, robespierre::CacheWrap.

robespierre::CacheWrap is a RawEventHandler, and also forwards the events to another RawEventHandler after it updates the cache.

To use it:

// src/main.rs
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::events::ReadyEvent;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;

    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let handler = Handler;

    let handler = CacheWrap::new(EventHandlerWrap::new(handler));
    connection.run(context, handler).await?;

    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {
    async fn on_ready(&self, ctx: Context, ready: ReadyEvent) {
        tracing::info!("We're ready!");
    }
}

Now to finish, let's make the bot reply with "pong" whenever someone says "ping":

// src/main.rs
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;
use robespierre_models::channels::MessageContent;
use robespierre_models::events::ReadyEvent;

// --snip--
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;

    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let handler = Handler;

    let handler = CacheWrap::new(EventHandlerWrap::new(handler));
    connection.run(context, handler).await?;

    Ok(())
}


#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {
    async fn on_ready(&self, ctx: Context, ready: ReadyEvent) {
        tracing::info!("We're ready!");
    }

    async fn on_message(&self, ctx: Context, message: Message) {
        if matches!(&message.content, MessageContent::Content(s) if s == "ping") {
            let _ = message.reply(&ctx, "pong").await;
        }
    }
}

Now, let's run it:

TOKEN=... cargo run

For the source code of this, see the ping-reply-pong example.

Framework

Sometimes, matching against a lot of commands in the message handler can be cumbersome, and we just want to call functions for each command.

The standard framework can help us a lot then:

Let's take our example from where we left off in the first chapter:

// src/main.rs
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;
use robespierre_models::channels::MessageContent;
use robespierre_models::events::ReadyEvent;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);
    
    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;
    
    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());
    
    let handler = Handler;
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));
    
    connection.run(context, handler).await?;

    Ok(())
}


#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {
    async fn on_ready(&self, ctx: Context, ready: ReadyEvent) {
        tracing::info!("We're ready!");
    }

    async fn on_message(&self, ctx: Context, message: Message) {
        if matches!(&message.content, MessageContent::Content(s) if s == "ping") {
            let _ = message.reply(&ctx, "pong").await;
        }
    }
}

Now, let's get rid of the on_ready and on_message functions:

// src/main.rs
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

// --snip--
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());
    let handler = Handler;
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));
    connection.run(context, handler).await?;

    Ok(())
}


#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

The signature of an event handler is defined as the robespierre::framework::standard::CommandCodeFn type, and looks like this:

fn command<'a>(
    ctx: &'a FwContext, // similar to Context
    msg: &'a Arc<Message>,
    args: &'a str,
) -> Pin<Box<dyn Future<Output=CommandResult> + Send + 'a>>;

To hide the nastier implementation details, the robespierre::framework::standard::macros::command macro helps a little, and turns this:

#[command]
async fn command(
    ctx: &FwContext,
    msg: &Message,
) -> CommandResult {
    Ok(())
}

Into a function that can be given where robespierre::framework::standard::CommandCodeFn is expected.

For parsing arguments, see the chapter on extractors, but for now we'll just ignore them.

First, let's start with a ping command:

// src/main.rs
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

// --snip--
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let handler = Handler;
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));
    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn ping(ctx: &FwContext, msg: &Message) -> CommandResult {
    msg.reply(ctx, "pong").await?;
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

Now, let's go build a StandardFramework:

// src/main.rs
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("ping", ping as CommandCodeFn))
        });

    // --snip--
    let handler = Handler;
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));
    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn ping(ctx: &FwContext, msg: &Message) -> CommandResult {
    msg.reply(ctx, "pong").await?;
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

And now let's also use it:

// src/main.rs
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre::FrameworkWrap;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("ping", ping as CommandCodeFn))
        });

    let handler = FrameworkWrap::new(fw, Handler);
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));

    // --snip--
    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn ping(ctx: &FwContext, msg: &Message) -> CommandResult {
    msg.reply(ctx, "pong").await?;
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

Then run:

TOKEN=... cargo run

And send "!ping" in a channel where the bot can see it. If everything went correctly, the bot should reply with "pong" to your message.

For the source code of this, see the the framework-example-bot example.

User data

Let's start with the code from the previous chapter:

// src/main.rs
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre::FrameworkWrap;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("ping", ping as CommandCodeFn))
        });

    let handler = FrameworkWrap::new(fw, Handler);
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));

    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn ping(ctx: &FwContext, msg: &Message) -> CommandResult {
    msg.reply(ctx, "pong").await?;
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

Specifically this line is of interest:

// src/main.rs
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre::FrameworkWrap;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    // --snip--
    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("ping", ping as CommandCodeFn))
        });

    let handler = FrameworkWrap::new(fw, Handler);
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));

    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn ping(ctx: &FwContext, msg: &Message) -> CommandResult {
    msg.reply(ctx, "pong").await?;
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

So let's add a "command counter", just a number to count how many commands we executed.

For performance, instead of using an Arc<Mutex<usize>>, we'll use an Arc<AtomicUsize>.

First, we'll start with a definition of a typemap key:

// src/main.rs
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre::FrameworkWrap;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

struct CommandCounterKey;

impl robespierre::typemap::Key for CommandCounterKey {
    type Value = Arc<AtomicUsize>;
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("ping", ping as CommandCodeFn))
        });

    let handler = FrameworkWrap::new(fw, Handler);
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));

    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn ping(ctx: &FwContext, msg: &Message) -> CommandResult {
    msg.reply(ctx, "pong").await?;
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

And add it to the map:

// src/main.rs
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre::FrameworkWrap;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

// --snip--
struct CommandCounterKey;

impl robespierre::typemap::Key for CommandCounterKey {
    type Value = Arc<AtomicUsize>;
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let mut data = robespierre::typemap::ShareMap::custom();
    data.insert::<CommandCounterKey>(Arc::new(AtomicUsize::new(0)));
    let context = Context::new(http, data).with_cache(CacheConfig::default());

    // --snip--
    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("ping", ping as CommandCodeFn))
        });

    let handler = FrameworkWrap::new(fw, Handler);
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));

    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn ping(ctx: &FwContext, msg: &Message) -> CommandResult {
    msg.reply(ctx, "pong").await?;
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

And when we get a command we have to increment it:

// src/main.rs
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::UserData;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre::FrameworkWrap;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

// --snip--
struct CommandCounterKey;

impl robespierre::typemap::Key for CommandCounterKey {
    type Value = Arc<AtomicUsize>;
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let mut data = robespierre::typemap::ShareMap::custom();
    data.insert::<CommandCounterKey>(Arc::new(AtomicUsize::new(0)));
    let context = Context::new(http, data).with_cache(CacheConfig::default());

    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("ping", ping as CommandCodeFn))
        });

    let handler = FrameworkWrap::new(fw, Handler);
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));

    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn ping(ctx: &FwContext, msg: &Message) -> CommandResult {
    // --snip--
    msg.reply(ctx, "pong").await?;

    let data = ctx.data_lock_read().await;
    let counter = data.get::<CommandCounterKey>().unwrap();
    counter.fetch_add(1, Ordering::SeqCst);
    
    // --snip--
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

And finally, let's add another command to display the counter:

// src/main.rs
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::UserData;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre::FrameworkWrap;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;

// --snip--
struct CommandCounterKey;

impl robespierre::typemap::Key for CommandCounterKey {
    type Value = Arc<AtomicUsize>;
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let mut data = robespierre::typemap::ShareMap::custom();
    data.insert::<CommandCounterKey>(Arc::new(AtomicUsize::new(0)));
    let context = Context::new(http, data).with_cache(CacheConfig::default());

    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("ping", ping as CommandCodeFn))
                .command(|| Command::new("command_counter", command_counter as CommandCodeFn))
        });

    // --snip--
    let handler = FrameworkWrap::new(fw, Handler);
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));

    connection.run(context, handler).await?;

    Ok(())
}

// --snip--
#[command]
async fn ping(ctx: &FwContext, msg: &Message) -> CommandResult {
    msg.reply(ctx, "pong").await?;

    let data = ctx.data_lock_read().await;
    let counter = data.get::<CommandCounterKey>().unwrap();
    counter.fetch_add(1, Ordering::SeqCst);
    
    Ok(())
}

#[command]
async fn command_counter(ctx: &FwContext, msg: &Message) -> CommandResult {
    let data = ctx.data_lock_read().await;
    let counter = data.get::<CommandCounterKey>().unwrap();
    let count = counter.fetch_add(1, Ordering::SeqCst); // this is itself a command,
                                                        // so fetch previous count and add one.
    msg.reply(ctx, format!("I received {} commands since I started running", count)).await?;
    
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

For the source code of this, see the framework-with-data-example.

Extractors

Extractors here work like they do in actix_web. They implement the FromMessage trait, and get the data they need from the message + context.

Note: The first 2 arguments are always the context and the message; all the others are expected to be extractors.

Note 2: They are only available when using the standard framework.

Here's an example of a stat user command, that just formats the user with the debug formatter and replies with the result:


#![allow(unused)]
fn main() {
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::extractors::{Args, Author, RawArgs, Rest};
use robespierre::model::MessageExt;
use robespierre_models::channels::Message;
use robespierre_models::users::User;

#[command]
async fn stat_user(ctx: &FwContext, message: &Message,
    Args((user,)): Args<(User,)> // parses a single argument as an UserId, and fetches the user with that id
) -> CommandResult {
    message.reply(ctx, format!("{:?}", user)).await?;

    Ok(())
}
}

Or a stat channel command:


#![allow(unused)]
fn main() {
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::extractors::{Args, Author, RawArgs, Rest};
use robespierre::model::MessageExt;
use robespierre_models::channels::{Message, Channel};

#[command]
async fn stat_channel(ctx: &FwContext, message: &Message,
    Args((channel,)): Args<(Channel,)> // parses a single argument as a ChannelId, and fetches the channel with that id
) -> CommandResult {
    message.reply(ctx, format!("{:?}", channel)).await?;

    Ok(())
}
}

Or a "repeat" command, which just echoes back the arguments (be careful who you let to run this command):


#![allow(unused)]
fn main() {
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::extractors::{Args, Author, RawArgs, Rest};
use robespierre::model::MessageExt;
use robespierre_models::channels::Message;

#[command]
async fn repeat(ctx: &FwContext, message: &Message, Author(author): Author, RawArgs(args): RawArgs) -> CommandResult {
    if author.id != "<your user id>" {
        return Ok(());
    }

    message.reply(ctx, &*args).await?;

    Ok(())
}
}

They get added to the framework in the exact same way.

By default, the delimiter Args uses is ``, but you can change it like:


#![allow(unused)]
fn main() {
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::extractors::{Args, Author, RawArgs, Rest};
use robespierre::model::MessageExt;
use robespierre_models::channels::Message;

#[command]
async fn repeat_with_spaces(
    ctx: &FwContext,
    message: &Message,
    Author(author): Author,
    #[delimiter(" ")] // it's the default, can be removed
    Args((arg1, arg2)): Args<(String, String)>
) -> CommandResult {
    if author.id != "<your user id>" {
        return Ok(());
    }

    message.reply(ctx, format!("first: {}, second: {}", arg1, arg2)).await?;

    Ok(())
}

#[command]
async fn repeat_with_spaces_and_tabs(
    ctx: &FwContext,
    message: &Message,
    Author(author): Author,
    #[delimiters(" ", "\t")]
    Args((arg1, arg2)): Args<(String, String)>
) -> CommandResult {
    if author.id != "<your user id>" {
        return Ok(());
    }

    message.reply(ctx, format!("first: {}, second: {}", arg1, arg2)).await?;

    Ok(())
}

#[command]
async fn repeat_with_spaces_and_tabs_2(
    ctx: &FwContext,
    message: &Message,
    Author(author): Author,
    #[delimiter(" ")]
    #[delimiter("\t")] // they cumulate
    Args((arg1, arg2)): Args<(String, String)>
) -> CommandResult {
    if author.id != "<your user id>" {
        return Ok(());
    }

    message.reply(ctx, format!("first: {}, second: {}", arg1, arg2)).await?;

    Ok(())
}

#[command]
async fn repeat_with_commas(
    ctx: &FwContext,
    message: &Message,
    Author(author): Author,
    #[delimiter(",")]
    Args((arg1, arg2)): Args<(String, String)>
) -> CommandResult {
    if author.id != "<your user id>" {
        return Ok(());
    }

    message.reply(ctx, format!("first: {}, second: {}", arg1, arg2)).await?;

    Ok(())
}
}

Note: valid values include everything that implements Into<Cow<'static, str>>, but for #[delimiters()], they have to all be of the same type.

If you need values of multiple types, then you can use multiple #[delimiters] attributes.

Special types

There are 2 kinds of special argument types (omitting .to_string() calls for simplicity):

Option<T> where T: Arg

Means "try to parse the argument as T, and if failed, pass None as arg, and continue trying to parse as if there was no arg here".

E.g. a type like:

// --snip--
fn f(
#[delimiter(",")]
args: Args<(String, Option<UserId>, String)>,
)->{}

If given aaa, bbb, it will result Args(("aaa", None, "bbb")).

While if given aaa, <@AAAAAAAAAAAAAAAAAAAAAAAAAA>, bbb, it will result Args(("aaa", Some(...), "bbb")).

Rest<T> where T: Arg

It means "use all the remaining text" to parse T.

E.g. a type like:

// --snip--
fn f(
#[delimiter(",")]
args: Args<(String, Rest<String>)>
)->{}

Given:

"aaa, bbb" => Args(("aaa", Rest("bbb")))
"aaa, bbb, ccc" => Args(("aaa", Rest("bbb, ccc")))

Note: No argument should ever come after a Rest<T>.

A full working example:

use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre::framework::standard::extractors::{Args, Author, RawArgs, Rest};
use robespierre::FrameworkWrap;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::users::User;
use robespierre_models::channels::{Channel, Message};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("stat_user", stat_user as CommandCodeFn))
                .command(|| Command::new("stat_channel", stat_channel as CommandCodeFn))
                .command(|| Command::new("repeat", repeat as CommandCodeFn))
                .command(|| Command::new("repeat_with_commas", repeat_with_commas as CommandCodeFn))
                .command(|| Command::new("repeat_with_commas_using_rest", repeat_with_commas_using_rest as CommandCodeFn))
        });
    let handler = FrameworkWrap::new(fw, Handler);
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));
    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn stat_user(ctx: &FwContext, message: &Message,
    Args((user,)): Args<(User,)> // parses a single argument as an UserId, and fetches the user with that id
) -> CommandResult {
    message.reply(ctx, format!("{:?}", user)).await?;

    Ok(())
}

#[command]
async fn stat_channel(ctx: &FwContext, message: &Message,
    Args((channel,)): Args<(Channel,)> // parses a single argument as a ChannelId, and fetches the channel with that id
) -> CommandResult {
    message.reply(ctx, format!("{:?}", channel)).await?;

    Ok(())
}

#[command]
async fn repeat(ctx: &FwContext, message: &Message, Author(author): Author, RawArgs(args): RawArgs) -> CommandResult {
    if author.id != "<your user id>" {
        return Ok(());
    }

    // args is an Arc<String>
    message.reply(ctx, &*args).await?;

    Ok(())
}

#[command]
async fn repeat_with_commas(
    ctx: &FwContext,
    message: &Message,
    Author(author): Author,
    #[delimiter(",")]
    Args((arg1, arg2)): Args<(String, String)>
) -> CommandResult {
    if author.id != "<your user id>" {
        return Ok(());
    }

    message.reply(ctx, format!("first: {}, second: {}", arg1, arg2)).await?;

    Ok(())
}

#[command]
async fn repeat_with_commas_using_rest(
    ctx: &FwContext,
    message: &Message,
    Author(author): Author,
    #[delimiter(",")]
    Args((arg1, Rest(arg2))): Args<(String, Rest<String>)>
) -> CommandResult {
    if author.id != "<your user id>" {
        return Ok(());
    }

    message.reply(ctx, format!("first: {}, second: {}", arg1, arg2)).await?;

    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}

As always, you can find the example in the repo

Writing custom FromMessage impls

Let's start with the basic imports:

use std::future::{Ready, ready};

use robespierre::framework::standard::{FwContext, CommandResult, CommandError};
use robespierre::framework::standard::extractors::{Msg, FromMessage};

#[tokio::main]
async fn main() {}

Let's say we want to get the whole message content:

use std::future::{Ready, ready};

use robespierre::framework::standard::{FwContext, CommandResult, CommandError};
use robespierre::framework::standard::extractors::{Msg, FromMessage};
use robespierre_models::channels::MessageContent;

// --snip--
#[tokio::main]
async fn main() {}

pub struct WholeMessageContent(pub String);

#[derive(Debug, thiserror::Error)]
#[error("message has no content")]
struct HasNoContent;

impl FromMessage for WholeMessageContent {
    type Config = ();
    type Fut = Ready<CommandResult<Self>>;

    fn from_message(ctx: FwContext, msg: Msg, _config: Self::Config) -> Self::Fut {
        let result = match &msg.message.content {
            MessageContent::Content(s) => Ok::<_, CommandError>(Self(s.clone())),
            _ => Err(HasNoContent.into()),
        };

        ready(result)
    }
}

And then everything you have to do is use it:

use std::future::{Ready, ready};

use robespierre::framework::standard::{FwContext, CommandResult, CommandError};
use robespierre::framework::standard::extractors::{Msg, FromMessage};
use robespierre::framework::standard::macros::{command};
use robespierre::model::MessageExt;
use robespierre_models::channels::MessageContent;
use robespierre_models::channels::Message;

// --snip--
#[tokio::main]
async fn main() {}

pub struct WholeMessageContent(pub String);

#[derive(Debug, thiserror::Error)]
#[error("message has no content")]
struct HasNoContent;

impl FromMessage for WholeMessageContent {
    type Config = ();
    type Fut = Ready<CommandResult<Self>>;

    fn from_message(ctx: FwContext, msg: Msg, _config: Self::Config) -> Self::Fut {
        let result = match &msg.message.content {
            MessageContent::Content(s) => Ok::<_, CommandError>(Self(s.clone())),
            _ => Err(HasNoContent.into()),
        };

        ready(result)
    }
}

#[command]
async fn cmd(ctx: &FwContext, message: &Message, WholeMessageContent(content): WholeMessageContent) -> CommandResult {
    message.reply(ctx, format!(r#"Whole message content is "{}""#, content));
    Ok(())
}

Note on permissions

There are 3 ways to check if an user / member has permissions to do something in a channel / server.

Let's say we want to implement a ban command:

Manually checking permissions


#![allow(unused)]
fn main() {
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::extractors::{Args, AuthorMember, RawArgs, Rest};
use robespierre::model::MessageExt;
use robespierre_models::channels::Message;
use robespierre_models::servers::ServerPermissions;
use robespierre_models::users::User;

#[derive(thiserror::Error, Debug)]
#[error("missing perms")]
struct MissingPerms;

#[command]
async fn ban(
    ctx: &FwContext,
    msg: &Message,
    Args((user, Rest(reason))): Args<(User, Rest<Option<String>>)>,
    AuthorMember(member): AuthorMember,
) -> CommandResult {
    let server = msg.server(ctx).await?.unwrap();
    
    let result = robespierre_models::permissions_utils::member_has_permissions(
        &member,
        ServerPermissions::BAN_MEMBERS,
        &server,
    );

    if !result { return Err(MissingPerms.into()); }

    // ban
    Ok(())
}
}

If you need to check if an user has permissions in a given channel use member_has_permissions_in_channel, and pass the ChannelPermissions you want to check + a reference to the channel you want to check for permissions in.

Extractor version


#![allow(unused)]
fn main() {
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::extractors::{Args, AuthorMember, RawArgs, Rest, RequiredPermissions};
use robespierre::model::MessageExt;
use robespierre_models::channels::Message;
use robespierre_models::channels::ChannelPermissions;
use robespierre_models::servers::ServerPermissions;
use robespierre_models::users::User;

#[command]
async fn ban(
    ctx: &FwContext,
    msg: &Message,
    Args((user, Rest(reason))): Args<(User, Rest<Option<String>>)>,
    _: RequiredPermissions<
        { ServerPermissions::bits(&ServerPermissions::BAN_MEMBERS) },
        { ChannelPermissions::bits(&ChannelPermissions::empty()) },
    >,
) -> CommandResult {
    // ban
    Ok(())
}
}

If one of them is T::bits(&T::empty()), it can be rewritten with the RequiredServerPermissions and RequiredChannelPermissions utils. In our example:


#![allow(unused)]
fn main() {
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::extractors::{Args, AuthorMember, RawArgs, Rest};
use robespierre::model::MessageExt;
use robespierre_models::channels::Message;
use robespierre_models::servers::ServerPermissions;
use robespierre_models::users::User;

use robespierre::framework::standard::extractors::RequiredServerPermissions;

#[command]
async fn ban(
    ctx: &FwContext,
    msg: &Message,
    Args((user, Rest(reason))): Args<(User, Rest<Option<String>>)>,
    _: RequiredServerPermissions<{ ServerPermissions::bits(&ServerPermissions::BAN_MEMBERS) }>,
) -> CommandResult {
    // --snip--
    // ban
    Ok(())
}
}

Adding required permissions to the command when creating the framework

use robespierre::CacheWrap;
use robespierre::EventHandlerWrap;
use robespierre::Context;
use robespierre::Authentication;
use robespierre::model::MessageExt;
use robespierre::framework::standard::{FwContext, CommandResult, macros::command};
use robespierre::framework::standard::{StandardFramework, Command, CommandCodeFn};
use robespierre::framework::standard::extractors::{Args, Rest};
use robespierre::FrameworkWrap;
use robespierre_cache::CacheConfig;
use robespierre_events::Connection;
use robespierre_http::Http;
use robespierre_models::channels::Message;
use robespierre_models::servers::ServerPermissions;
use robespierre_models::users::User;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // --snip--
    tracing_subscriber::fmt::init();

    let token = std::env::var("TOKEN")
        .expect("Cannot get token; set environment variable TOKEN=... and run again");

    let auth = Authentication::bot(token);

    let http = Http::new(&auth).await?;
    let connection = Connection::connect(&auth).await?;

    let context = Context::new(http, robespierre::typemap::ShareMap::custom()).with_cache(CacheConfig::default());

    let fw = StandardFramework::default()
        .configure(|c| c.prefix("!"))
        .group(|g| {
            g.name("General")
                .command(|| Command::new("ban", ban as CommandCodeFn).required_server_permissions(ServerPermissions::BAN_MEMBERS))
        });

    // --snip--
    let handler = FrameworkWrap::new(fw, Handler);
    let handler = CacheWrap::new(EventHandlerWrap::new(handler));

    connection.run(context, handler).await?;

    Ok(())
}

#[command]
async fn ban(
    ctx: &FwContext,
    msg: &Message,
    Args((user, Rest(reason))): Args<(User, Rest<Option<String>>)>,
) -> CommandResult {
    // ban
    Ok(())
}

#[derive(Clone)]
struct Handler;

#[robespierre::async_trait]
impl robespierre::EventHandler for Handler {}