From e4bba55eff7ce775915d2c8295dc6ac286b3916d Mon Sep 17 00:00:00 2001 From: oddgrd <29732646+oddgrd@users.noreply.github.com> Date: Sun, 14 Aug 2022 20:47:57 +0200 Subject: [PATCH] feat: serenity integration - implement Service for serenity::Client to deploy discord bots with shuttle - add hello-world and postgres examples --- examples/serenity/hello_world/Cargo.toml | 8 ++ examples/serenity/hello_world/Shuttle.toml | 1 + examples/serenity/hello_world/src/lib.rs | 40 ++++++++ examples/serenity/postgres/Cargo.toml | 10 ++ examples/serenity/postgres/Shuttle.toml | 1 + examples/serenity/postgres/schema.sql | 7 ++ examples/serenity/postgres/src/lib.rs | 105 +++++++++++++++++++++ service/Cargo.toml | 3 + service/src/lib.rs | 13 +++ 9 files changed, 188 insertions(+) create mode 100644 examples/serenity/hello_world/Cargo.toml create mode 100644 examples/serenity/hello_world/Shuttle.toml create mode 100644 examples/serenity/hello_world/src/lib.rs create mode 100644 examples/serenity/postgres/Cargo.toml create mode 100644 examples/serenity/postgres/Shuttle.toml create mode 100644 examples/serenity/postgres/schema.sql create mode 100644 examples/serenity/postgres/src/lib.rs diff --git a/examples/serenity/hello_world/Cargo.toml b/examples/serenity/hello_world/Cargo.toml new file mode 100644 index 0000000000..2085e04e69 --- /dev/null +++ b/examples/serenity/hello_world/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello-world" +version = "0.1.0" +edition = "2021" + +[dependencies] +serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +shuttle-service = { version = "0.4.1", features = ["bot-serenity"] } diff --git a/examples/serenity/hello_world/Shuttle.toml b/examples/serenity/hello_world/Shuttle.toml new file mode 100644 index 0000000000..39fe098344 --- /dev/null +++ b/examples/serenity/hello_world/Shuttle.toml @@ -0,0 +1 @@ +name = "hello-world-serenity-bot" diff --git a/examples/serenity/hello_world/src/lib.rs b/examples/serenity/hello_world/src/lib.rs new file mode 100644 index 0000000000..500a3964ea --- /dev/null +++ b/examples/serenity/hello_world/src/lib.rs @@ -0,0 +1,40 @@ +use serenity::async_trait; +use serenity::model::channel::Message; +use serenity::model::gateway::Ready; +use serenity::prelude::*; +use std::env; + +struct Bot; + +#[async_trait] +impl EventHandler for Bot { + async fn message(&self, ctx: Context, msg: Message) { + if msg.content == "!hello" { + if let Err(why) = msg.channel_id.say(&ctx.http, "world!").await { + println!("Error sending message: {:?}", why); + } + } + } + + async fn ready(&self, _: Context, ready: Ready) { + println!("{} is connected!", ready.user.name); + } +} + +#[shuttle_service::main] +async fn serenity() -> shuttle_service::ShuttleSerenity { + // Configure the client with your Discord bot token in the environment. + let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + + // Set gateway intents, which decides what events the bot will be notified about + let intents = GatewayIntents::GUILD_MESSAGES + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::MESSAGE_CONTENT; + + let client = Client::builder(&token, intents) + .event_handler(Bot) + .await + .expect("Err creating client"); + + Ok(client) +} diff --git a/examples/serenity/postgres/Cargo.toml b/examples/serenity/postgres/Cargo.toml new file mode 100644 index 0000000000..16beef752d --- /dev/null +++ b/examples/serenity/postgres/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "serenity-postgres" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = "1.0" +serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } +shuttle-service = { version = "0.4.1", features = ["sqlx-postgres", "bot-serenity"] } +sqlx = { version = "0.5", features = ["runtime-tokio-native-tls", "postgres"] } diff --git a/examples/serenity/postgres/Shuttle.toml b/examples/serenity/postgres/Shuttle.toml new file mode 100644 index 0000000000..bc686807da --- /dev/null +++ b/examples/serenity/postgres/Shuttle.toml @@ -0,0 +1 @@ +name = "postgres-serenity-bot" diff --git a/examples/serenity/postgres/schema.sql b/examples/serenity/postgres/schema.sql new file mode 100644 index 0000000000..2e5ebbe0db --- /dev/null +++ b/examples/serenity/postgres/schema.sql @@ -0,0 +1,7 @@ +DROP TABLE IF EXISTS todos; + +CREATE TABLE todos ( + id serial PRIMARY KEY, + user_id BIGINT NULL, + note TEXT NOT NULL +); diff --git a/examples/serenity/postgres/src/lib.rs b/examples/serenity/postgres/src/lib.rs new file mode 100644 index 0000000000..e74339147d --- /dev/null +++ b/examples/serenity/postgres/src/lib.rs @@ -0,0 +1,105 @@ +use serenity::async_trait; +use serenity::model::prelude::*; +use serenity::prelude::*; +use shuttle_service::error::CustomError; +use sqlx::{Executor, FromRow, PgPool}; +use std::env; + +struct Bot { + database: PgPool, +} + +#[derive(FromRow)] +struct Todo { + pub id: i32, + pub note: String, +} + +#[async_trait] +impl EventHandler for Bot { + async fn message(&self, ctx: Context, msg: Message) { + // The user_id of the user sending a command + let user_id = msg.author.id.0 as i64; + + // Add a new todo using `~todo add ` and persist it in postgres + if let Some(note) = msg.content.strip_prefix("~todo add") { + let note = note.trim(); + sqlx::query("INSERT INTO todos (note, user_id) VALUES ($1, $2)") + .bind(note) + .bind(user_id) + .execute(&self.database) + .await + .unwrap(); + + let response = format!("Added `{}` to your todo list", note); + msg.channel_id.say(&ctx, response).await.unwrap(); + + // Remove a todo by calling `~todo remove ` with the index of the todo you want to remove + // from the `~todo list` output + } else if let Some(todo_index) = msg.content.strip_prefix("~todo remove") { + let todo_index = todo_index.trim().parse::().unwrap() - 1; + + let todo: Todo = sqlx::query_as( + "SELECT id, note FROM todos WHERE user_id = $1 ORDER BY id LIMIT 1 OFFSET $2", + ) + .bind(user_id) + .bind(todo_index) + .fetch_one(&self.database) + .await + .unwrap(); + + sqlx::query("DELETE FROM todos WHERE id = $1") + .bind(todo.id) + .execute(&self.database) + .await + .unwrap(); + + let response = format!("Completed `{}`!", todo.note); + msg.channel_id.say(&ctx, response).await.unwrap(); + + // List the calling users todos using ยด~todo list` + } else if msg.content.trim() == "~todo list" { + let todos: Vec = + sqlx::query_as("SELECT note, id FROM todos WHERE user_id = $1 ORDER BY id") + .bind(user_id) + .fetch_all(&self.database) + .await + .unwrap(); + + let mut response = format!("You have {} pending todos:\n", todos.len()); + for (i, todo) in todos.iter().enumerate() { + response += &format!("{}. {}\n", i + 1, todo.note); + } + + msg.channel_id.say(&ctx, response).await.unwrap(); + } + } + + async fn ready(&self, _: Context, ready: Ready) { + println!("{} is connected!", ready.user.name); + } +} + +#[shuttle_service::main] +async fn serenity(#[shared::Postgres] pool: PgPool) -> shuttle_service::ShuttleSerenity { + // Configure the client with your Discord bot token in the environment. + let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + + // Run the schema migration + pool.execute(include_str!("../schema.sql")) + .await + .map_err(CustomError::new)?; + + // Set gateway intents, which decides what events the bot will be notified about + let intents = GatewayIntents::GUILD_MESSAGES + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::MESSAGE_CONTENT; + + let bot = Bot { database: pool }; + let client = Client::builder(&token, intents) + .event_handler(bot) + .await + .expect("Err creating client"); + + Ok(client) +} diff --git a/service/Cargo.toml b/service/Cargo.toml index 4803db910a..dbd2019cd4 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -20,6 +20,7 @@ lazy_static = "1.4.0" libloading = { version = "0.7.3", optional = true } log = "0.4.17" paste = "1.0.7" +serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"], optional = true } poem = { version = "1.3.35", optional = true } regex = "1.5.6" rocket = { version = "0.5.0-rc.2", optional = true } @@ -67,3 +68,5 @@ web-rocket = ["rocket"] web-tide = ["tide"] web-tower = ["tower", "hyper"] web-poem = ["poem"] + +bot-serenity = ["serenity"] diff --git a/service/src/lib.rs b/service/src/lib.rs index 358dd85d41..d2cef3c294 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -514,4 +514,17 @@ where } } +#[cfg(feature = "bot-serenity")] +#[async_trait] +impl Service for serenity::Client { + async fn bind(mut self: Box, _addr: SocketAddr) -> Result<(), error::Error> { + self.start().await.map_err(error::CustomError::new)?; + + Ok(()) + } +} + +#[cfg(feature = "bot-serenity")] +pub type ShuttleSerenity = Result; + pub const VERSION: &str = env!("CARGO_PKG_VERSION");