Skip to content

Commit

Permalink
feat: serenity integration
Browse files Browse the repository at this point in the history
 - implement Service for serenity::Client to deploy discord bots with shuttle

 - add hello-world and postgres examples
  • Loading branch information
oddgrd committed Aug 24, 2022
1 parent 1e798c9 commit 46bff1e
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 0 deletions.
8 changes: 8 additions & 0 deletions examples/serenity/hello_world/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
1 change: 1 addition & 0 deletions examples/serenity/hello_world/Shuttle.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name = "hello-world-serenity-bot"
40 changes: 40 additions & 0 deletions examples/serenity/hello_world/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 10 additions & 0 deletions examples/serenity/postgres/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
1 change: 1 addition & 0 deletions examples/serenity/postgres/Shuttle.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name = "postgres-serenity-bot"
7 changes: 7 additions & 0 deletions examples/serenity/postgres/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DROP TABLE IF EXISTS todos;

CREATE TABLE todos (
id serial PRIMARY KEY,
user_id BIGINT NULL,
note TEXT NOT NULL
);
105 changes: 105 additions & 0 deletions examples/serenity/postgres/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 <note>` 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 <index>` 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::<i64>().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<Todo> =
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)
}
3 changes: 3 additions & 0 deletions service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ libloading = { version = "0.7.3", optional = true }
log = "0.4.17"
mongodb = { version = "2.3.0", optional = true }
paste = "1.0.8"
serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"], optional = true }
poem = { version = "1.3.40", optional = true }
regex = "1.6.0"
rocket = { version = "0.5.0-rc.2", optional = true }
Expand Down Expand Up @@ -69,3 +70,5 @@ web-rocket = ["rocket"]
web-tide = ["tide"]
web-tower = ["tower", "hyper"]
web-poem = ["poem"]

bot-serenity = ["serenity"]
13 changes: 13 additions & 0 deletions service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,4 +515,17 @@ where
}
}

#[cfg(feature = "bot-serenity")]
#[async_trait]
impl Service for serenity::Client {
async fn bind(mut self: Box<Self>, _addr: SocketAddr) -> Result<(), error::Error> {
self.start().await.map_err(error::CustomError::new)?;

Ok(())
}
}

#[cfg(feature = "bot-serenity")]
pub type ShuttleSerenity = Result<serenity::Client, Error>;

pub const VERSION: &str = env!("CARGO_PKG_VERSION");

0 comments on commit 46bff1e

Please sign in to comment.