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.