Skip to content

Discord Bot connected to a Dojo World

This example explains how to use the Dojo Rust library to connect a discord bot to a Dojo World. This example uses the shuttle runtime to deploy the bot.

You will need to have a discord bot token to use this example. You can get one by creating an application and bot in the discord developer portal.

Prerequisites

  • Download and install the rust compiler Rust.

  • Setup a Rust project :

  • Install the shuttle cli Shuttle

Setup

  • Create a Secrets.toml file in the root of the project with the following:
DISCORD_TOKEN = "your_discord_token_here"
TORII_URL = "your_torii_url_here"
NODE_URL = "your_node_url_here"
TORII_RELAY_URL = "your_torii_relay_url_here"
WORLD_ADDRESS = "your_world_address_here"
CHANNEL_ID = "your_discord_channel_id_here"
  • Add the following to your Cargo.toml file:
[dependencies]
poise = { version = "0.6.1" }
toml = { version = "0.7", default-features = false, features = ["parse", "display"] }
starknet = "0.12.0"
starknet-crypto = "0.7.0"
starknet-types-core = "~0.1.4"
dojo-types = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-rc.1" }
torii-client = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-rc.1" }
torii-grpc = { git = "https://github.com/dojoengine/dojo", features = [
    "client",
], tag = "v1.0.0-rc.1" }
torii-relay = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-rc.1" }
dojo-world = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-rc.1" }
 
cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.4.7", features = ["abigen-rs"] }
cainome-cairo-serde = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.4.7" }
cairo-lang-filesystem = "=2.8.4"
scarb = { git = "https://github.com/software-mansion/scarb", tag = "v2.8.4" }
shuttle-runtime = { version = "0.48.0" }
shuttle-serenity = { version = "0.48.0" }
shuttle-rocket = { version = "0.48.0" }
serenity = { version = "0.12.0", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] }
tokio = "1.26.0"

Code

use std::{num::NonZero, sync::Arc, time::Duration};
 
use serenity::{
    all::{ChannelId, CreateMessage, GatewayIntents, Http},
    futures::StreamExt,
    Client,
};
use shuttle_runtime::SecretStore;
use starknet_crypto::Felt;
use torii_grpc::types::{EntityKeysClause, KeysClause};
 
pub type Error = Box<dyn std::error::Error + Send + Sync>;
pub type Context<'a> = poise::Context<'a, Data, Error>;
 
pub struct Data {}
 
#[poise::command(slash_command)]
pub async fn hello(ctx: Context<'_>) -> Result<(), Error> {
    // Add any logic here, fetch user in database (https://docs.shuttle.rs/resources/shuttle-shared-db)
 
    ctx.say("Hello, world!").await?;
    Ok(())
}
 
struct Config {
    discord_token: String,
    channel_id: NonZero<u64>,
    torii_url: String,
    node_url: String,
    torii_relay_url: String,
    world_address: String,
}
 
impl Config {
    pub fn from_secrets(secret_store: SecretStore) -> Self {
        let discord_token = secret_store.get("DISCORD_TOKEN").unwrap();
        let channel_id = NonZero::new(
            secret_store
                .get("CHANNEL_ID")
                .unwrap()
                .parse::<u64>()
                .unwrap(),
        )
        .unwrap();
        let torii_url = secret_store.get("TORII_URL").unwrap();
        let node_url = secret_store.get("NODE_URL").unwrap();
        let torii_relay_url = secret_store.get("TORII_RELAY_URL").unwrap();
        let world_address = secret_store.get("WORLD_ADDRESS").unwrap();
 
        Config {
            discord_token,
            channel_id,
            torii_url,
            node_url,
            torii_relay_url,
            world_address,
        }
    }
}
 
#[shuttle_runtime::main]
async fn main(
    #[shuttle_runtime::Secrets] secret_store: SecretStore,
) -> shuttle_serenity::ShuttleSerenity {
    let intents = GatewayIntents::non_privileged();
 
    let config = Config::from_secrets(secret_store);
 
    let framework = poise::Framework::builder()
        .options(poise::FrameworkOptions {
            commands: vec![hello()],
            ..Default::default()
        })
        .setup(|ctx, _ready, framework| {
            Box::pin(async move {
                poise::builtins::register_globally(ctx, &framework.options().commands).await?;
 
                Ok(Data {})
            })
        })
        .build();
 
    let client = Client::builder(config.discord_token.clone(), intents)
        .framework(framework)
        .await
        .expect("Failed to build client");
 
    tokio::spawn(async move {
        let client = torii_client::client::Client::new(
            config.torii_url.clone(),
            config.node_url.clone(),
            config.torii_relay_url.clone(),
            Felt::from_hex_unchecked(&config.world_address.clone()),
        )
        .await
        .expect("Failed to create Torii client");
 
        subscribe(client, config).await;
    });
 
    Ok(client.into())
}
 
async fn subscribe(client: torii_client::client::Client, config: Config) {
    let mut tries = 0;
    let max_num_tries = 200;
 
    let mut backoff = Duration::from_secs(1);
    let max_backoff = Duration::from_secs(60);
 
    let http = Arc::new(Http::new(&config.discord_token.clone()));
 
    loop {
        let rcv: Result<
            torii_grpc::client::EntityUpdateStreaming,
            torii_client::client::error::Error,
        > = client
            .on_event_message_updated(
                vec![EntityKeysClause::Keys(KeysClause {
                    keys: vec![],
                    pattern_matching: torii_grpc::types::PatternMatching::VariableLen,
                    models: vec![],
                })],
                true,
            )
            .await;
 
        match rcv {
            Ok(mut rcv) => {
                backoff = Duration::from_secs(1);
 
                while let Some(Ok((_, entity))) = rcv.next().await {
                    // Do something with your events entity here
                    let entity_string = format!("{:?}", entity);
                    let content = CreateMessage::new().content(entity_string);
 
                    ChannelId::from(config.channel_id)
                        .send_message(http.clone(), content)
                        .await
                        .unwrap();
                }
            }
            Err(_) => {
                println!("Subscription was lost, attempting to reconnect");
                tries += 1;
            }
        }
 
        tokio::time::sleep(backoff).await;
        backoff = std::cmp::min(backoff * 2, max_backoff);
 
        if tries >= max_num_tries {
            println!("Max number of tries reached, exiting");
            break;
        }
    }
 
    println!("Torii client disconnected");
}

Deploy and local testing

shuttle deploy

shuttle run