From 724d7467c3a99ab11e587f1bd8829593544cc9ff Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Tue, 14 Jun 2022 20:49:35 +0200 Subject: [PATCH] handle handlers --- Cargo.lock | 73 +++++----- Cargo.toml | 9 +- README.md | 2 + src/handler.rs | 363 +++++++++++++++++++++++++++---------------------- src/lawsuit.rs | 20 +++ src/main.rs | 48 +++++-- src/model.rs | 43 ++++++ 7 files changed, 350 insertions(+), 208 deletions(-) create mode 100644 src/lawsuit.rs create mode 100644 src/model.rs diff --git a/Cargo.lock b/Cargo.lock index d1cb52e..69a201c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,7 +164,6 @@ dependencies = [ "libc", "num-integer", "num-traits", - "serde", "time 0.1.44", "winapi", ] @@ -196,17 +195,6 @@ dependencies = [ "tracing-error", ] -[[package]] -name = "command_attr" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8862b532587a5efe6f20a750a0d01f390111f3570870581c13374024affc46dc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "court-bot" version = "0.1.0" @@ -214,6 +202,7 @@ dependencies = [ "color-eyre", "dotenv", "mongodb", + "serde", "serenity", "tokio", "tracing", @@ -717,12 +706,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "levenshtein" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" - [[package]] name = "libc" version = "0.2.126" @@ -769,6 +752,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.9" @@ -1070,6 +1062,30 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" + [[package]] name = "reqwest" version = "0.11.11" @@ -1325,12 +1341,9 @@ dependencies = [ "bitflags", "bytes", "cfg-if", - "chrono", - "command_attr", "dashmap", "flate2", "futures", - "levenshtein", "mime", "mime_guess", "parking_lot", @@ -1339,13 +1352,11 @@ dependencies = [ "serde", "serde-value", "serde_json", - "static_assertions", "time 0.3.9", "tokio", "tracing", "typemap_rev", "url", - "uwl", ] [[package]] @@ -1416,12 +1427,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stringprep" version = "0.1.2" @@ -1659,9 +1664,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" dependencies = [ "ansi_term", + "lazy_static", + "matchers", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] @@ -1826,12 +1835,6 @@ dependencies = [ "serde", ] -[[package]] -name = "uwl" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" - [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a771120..5c63a16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,12 @@ edition = "2021" color-eyre = "0.6.1" dotenv = "0.15.0" mongodb = "2.2.2" -serenity = { version = "0.11.2", features = ["collector"] } tokio = { version = "1.19.2", features = ["full"] } tracing = "0.1.35" -tracing-subscriber = "0.3.11" +tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } +serde = { version = "1.0.137", features = ["derive"] } + +[dependencies.serenity] +version = "0.11.2" +default-features = false +features = ["builder", "cache", "client", "gateway", "collector", "http", "rustls_backend", "tokio", "typemap_rev", "utils"] diff --git a/README.md b/README.md index f37561e..445275d 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,11 @@ DISCORD_TOKEN=token APPLICATION_ID=uwu RUST_LOG=DEBUG MONGO_URI=mongodb://localhost:27017 +DB_NAME=court_bot MONGO_INITDB_ROOT_USERNAME=root MONGO_INITDB_ROOT_PASSWORD=uwu DEV= +# SET_GLOBAL= ``` run mongodb diff --git a/src/handler.rs b/src/handler.rs index c3672aa..dc5390b 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,185 +1,230 @@ -use serenity::{async_trait, framework::standard::Command, model::prelude::*, prelude::*}; -use tracing::debug; +use color_eyre::eyre::{eyre, ContextCompat}; +use serenity::{ + async_trait, + builder::CreateApplicationCommands, + model::{ + interactions::application_command::ApplicationCommandOptionType, + prelude::{application_command::*, *}, + }, + prelude::*, +}; +use tracing::{debug, error, info}; + +use crate::{ + lawsuit::{Lawsuit, LawsuitState}, + WrapErr, +}; + +fn slash_commands(commands: &mut CreateApplicationCommands) -> &mut CreateApplicationCommands { + commands.create_application_command(|command| { + command + .name("lawsuit") + .description("Einen Gerichtsprozess starten") + .create_option(|option| { + option + .name("create") + .description("Einen neuen Gerichtsprozess anfangen") + .kind(ApplicationCommandOptionType::SubCommand) + .create_sub_option(|option| { + option + .name("plaintiff") + .description("Der Kläger") + .kind(ApplicationCommandOptionType::User) + .required(true) + }) + .create_sub_option(|option| { + option + .name("accused") + .description("Der Angeklagte") + .kind(ApplicationCommandOptionType::User) + .required(true) + }) + .create_sub_option(|option| { + option + .name("reason") + .description("Der Grund für die Klage") + .kind(ApplicationCommandOptionType::String) + .required(true) + }) + .create_sub_option(|option| { + option + .name("plaintiff_lawyer") + .description("Der Anwalt des Klägers") + .kind(ApplicationCommandOptionType::User) + .required(false) + }) + .create_sub_option(|option| { + option + .name("accused_lawyer") + .description("Der Anwalt des Angeklagten") + .kind(ApplicationCommandOptionType::User) + .required(false) + }) + }) + }) +} pub struct Handler { pub dev_guild_id: Option, + pub set_global_commands: bool, + pub mongo: Mongo, } #[async_trait] impl EventHandler for Handler { async fn ready(&self, ctx: Context, ready: Ready) { - println!("{} is connected!", ready.user.name); + info!(name = %ready.user.name, "Bot is connected!"); if let Some(guild_id) = self.dev_guild_id { - let commands = GuildId::set_application_commands(&guild_id, &ctx.http, |commands| { - commands - .create_application_command(|command| { - command.name("ping").description("A ping command") - }) - .create_application_command(|command| { - command.name("id").description("Get a user id").create_option(|option| { - option - .name("id") - .description("The user to lookup") - .kind(CommandOptionType::User) - .required(true) - }) - }) - .create_application_command(|command| { - command - .name("welcome") - .name_localized("de", "begrüßen") - .description("Welcome a user") - .description_localized("de", "Einen Nutzer begrüßen") - .create_option(|option| { - option - .name("user") - .name_localized("de", "nutzer") - .description("The user to welcome") - .description_localized("de", "Der zu begrüßende Nutzer") - .kind(CommandOptionType::User) - .required(true) - }) - .create_option(|option| { - option - .name("message") - .name_localized("de", "nachricht") - .description("The message to send") - .description_localized("de", "Die versendete Nachricht") - .kind(CommandOptionType::String) - .required(true) - .add_string_choice_localized( - "Welcome to our cool server! Ask me if you need help", - "pizza", - [("de", "Willkommen auf unserem coolen Server! Frag mich, falls du Hilfe brauchst")] - ) - .add_string_choice_localized( - "Hey, do you want a coffee?", - "coffee", - [("de", "Hey, willst du einen Kaffee?")], - ) - .add_string_choice_localized( - "Welcome to the club, you're now a good person. Well, I hope.", - "club", - [("de", "Willkommen im Club, du bist jetzt ein guter Mensch. Naja, hoffentlich.")], - ) - .add_string_choice_localized( - "I hope that you brought a controller to play together!", - "game", - [("de", "Ich hoffe du hast einen Controller zum Spielen mitgebracht!")], - ) - }) - }) - .create_application_command(|command| { - command - .name("numberinput") - .description("Test command for number input") - .create_option(|option| { - option - .name("int") - .description("An integer from 5 to 10") - .kind(CommandOptionType::Integer) - .min_int_value(5) - .max_int_value(10) - .required(true) - }) - .create_option(|option| { - option - .name("number") - .description("A float from -3.3 to 234.5") - .kind(CommandOptionType::Number) - .min_number_value(-3.3) - .max_number_value(234.5) - .required(true) - }) - }) - .create_application_command(|command| { - command - .name("attachmentinput") - .description("Test command for attachment input") - .create_option(|option| { - option - .name("attachment") - .description("A file") - .kind(CommandOptionType::Attachment) - .required(true) - }) - }) - }) - .await; + let guild_commands = + GuildId::set_application_commands(&guild_id, &ctx.http, slash_commands).await; - debug!(?commands, "I now have the following guild slash commands",); + match guild_commands { + Ok(_) => info!("Installed guild slash commands"), + Err(error) => error!(?error, "Failed to create global commands"), + } } - /* - let guild_command = Command::create_global_application_command(&ctx.http, |command| { - command - .name("wonderful_command") - .description("An amazing command") - }) - .await; - */ - - println!( - "I created the following global slash command: {:#?}", - guild_command - ); + if self.set_global_commands { + todo!() + // let guild_commands = + // ApplicationCommand::create_global_application_command(&ctx.http, slash_commands) + // .await; + // match guild_commands { + // Ok(commands) => info!(?commands, "Created global commands"), + // Err(error) => error!(?error, "Failed to create global commands"), + // } + } } async fn interaction_create(&self, ctx: Context, interaction: Interaction) { if let Interaction::ApplicationCommand(command) = interaction { - println!("Received command interaction: {:#?}", command); + debug!(name = %command.data.name, "Received command interaction"); - let content = match command.data.name.as_str() { - "ping" => "Hey, I'm alive!".to_string(), - "id" => { - let options = command - .data - .options - .get(0) - .expect("Expected user option") - .resolved - .as_ref() - .expect("Expected user object"); - - if let CommandDataOptionValue::User(user, _member) = options { - format!("{}'s id is {}", user.tag(), user.id) + let result = match command.data.name.as_str() { + "lawsuit" => { + let result = lawsuit_command_handler(&command).await; + if let Err(err) = result { + error!(?err, "Error processing response"); + command + .create_interaction_response(&ctx.http, |response| { + response + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|message| { + message.content("An error occurred") + }) + }) + .await + .wrap_err("error response") } else { - "Please provide a valid user".to_string() + Ok(()) } } - "attachmentinput" => { - let options = command - .data - .options - .get(0) - .expect("Expected attachment option") - .resolved - .as_ref() - .expect("Expected attachment object"); - - if let CommandDataOptionValue::Attachment(attachment) = options { - format!( - "Attachment name: {}, attachment size: {}", - attachment.filename, attachment.size - ) - } else { - "Please provide a valid attachment".to_string() - } - } - _ => "not implemented :(".to_string(), + _ => command + .create_interaction_response(&ctx.http, |response| { + response + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|message| { + message.content("not implemented :(") + }) + }) + .await + .wrap_err("not implemented response"), }; - - if let Err(why) = command - .create_interaction_response(&ctx.http, |response| { - response - .kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|message| message.content(content)) - }) - .await - { - println!("Cannot respond to slash command: {}", why); + if let Err(err) = result { + error!(?err, "Error sending response"); } } } } + +async fn lawsuit_command_handler( + command: &ApplicationCommandInteraction, +) -> color_eyre::Result<()> { + let options = &command.data.options; + let subcomamnd = options.get(0).wrap_err("needs subcommand")?; + + match subcomamnd.name.as_str() { + "create" => { + let options = &subcomamnd.options; + let plaintiff = get_user(options.get(0)).wrap_err("plaintiff")?; + let accused = get_user(options.get(1)).wrap_err("accused")?; + let reason = get_string(options.get(2)).wrap_err("reason")?; + let plaintiff_layer = get_user_optional(options.get(3)).wrap_err("plaintiff_layer")?; + let accused_layer = get_user_optional(options.get(4)).wrap_err("accused_layer")?; + + let lawsuit = Lawsuit { + plaintiff: plaintiff.0.id, + accused: accused.0.id, + plaintiff_layer: plaintiff_layer.map(|l| l.0.id), + accused_layer: accused_layer.map(|l| l.0.id), + reason: reason.to_owned(), + state: LawsuitState::Initial, + court_room: Default::default(), + }; + + info!(?lawsuit, "Created lawsuit"); + + Ok(()) + } + _ => Err(eyre!("Unknown subcommand")), + } +} + +fn get_user( + option: Option<&ApplicationCommandInteractionDataOption>, +) -> color_eyre::Result<(&User, &Option)> { + let option = get_user_optional(option); + match option { + Ok(Some(t)) => Ok(t), + Ok(None) => Err(eyre!("Expected value!")), + Err(err) => Err(err), + } +} + +fn get_user_optional( + option: Option<&ApplicationCommandInteractionDataOption>, +) -> color_eyre::Result)>> { + if let Some(option) = option { + if let Some(command) = option.resolved.as_ref() { + if let ApplicationCommandInteractionDataOptionValue::User(user, member) = command { + Ok(Some((user, member))) + } else { + Err(eyre!("Expected user!")) + } + } else { + Ok(None) + } + } else { + Ok(None) + } +} + +fn get_string( + option: Option<&ApplicationCommandInteractionDataOption>, +) -> color_eyre::Result<&str> { + let option = get_string_optional(option); + match option { + Ok(Some(t)) => Ok(t), + Ok(None) => Err(eyre!("Expected value!")), + Err(err) => Err(err), + } +} + +fn get_string_optional( + option: Option<&ApplicationCommandInteractionDataOption>, +) -> color_eyre::Result> { + if let Some(option) = option { + if let Some(command) = option.resolved.as_ref() { + if let ApplicationCommandInteractionDataOptionValue::String(str) = command { + Ok(Some(str)) + } else { + Err(eyre!("Expected string!")) + } + } else { + Ok(None) + } + } else { + Ok(None) + } +} diff --git a/src/lawsuit.rs b/src/lawsuit.rs new file mode 100644 index 0000000..551bc38 --- /dev/null +++ b/src/lawsuit.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; +use serenity::model::id::{ChannelId, UserId}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum LawsuitState { + Initial, + InProgress, + Completed, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Lawsuit { + pub plaintiff: UserId, + pub accused: UserId, + pub plaintiff_layer: Option, + pub accused_layer: Option, + pub reason: String, + pub state: LawsuitState, + pub court_room: ChannelId, +} diff --git a/src/main.rs b/src/main.rs index 10bc7da..99b97cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,31 +1,55 @@ mod handler; +mod lawsuit; +mod model; use std::env; use color_eyre::{eyre::WrapErr, Result}; use serenity::{model::prelude::*, prelude::*}; +use tracing::info; +use tracing_subscriber::EnvFilter; -use crate::handler::Handler; +use crate::{handler::Handler, model::Mongo}; #[tokio::main] -fn main() -> Result<()> { +async fn main() -> Result<()> { color_eyre::install()?; let _ = dotenv::dotenv(); + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .init(); + + info!("Starting up..."); + + let mongo_uri = env::var("MONGO_URI").wrap_err("MONGO_URI not found in the environment")?; + let db_name = env::var("DB_NAME").unwrap_or_else(|_| "court-bot".to_string()); + + let mongo = Mongo::connect(&mongo_uri, &db_name).await?; + + info!("Connected to mongodb"); + let token = env::var("DISCORD_TOKEN").wrap_err("DISCORD_TOKEN not found in environment")?; - let guild_id = if let Ok(_) = env::var("DEV") { - SOme( GuildId( - env::var("GUILD_ID") - .wrap_err("GUILD_ID not found in environment, must be set when DEV is set")? - .parse() - .wrap_err("GUILD_ID must be an integer")?, - )) - }) -} else {None}; + let dev_guild_id = if env::var("DEV").is_ok() { + Some(GuildId( + env::var("GUILD_ID") + .wrap_err("GUILD_ID not found in environment, must be set when DEV is set")? + .parse() + .wrap_err("GUILD_ID must be an integer")?, + )) + } else { + None + }; + + let set_global_commands = env::var("SET_GLOBAL").is_ok(); let mut client = Client::builder(token, GatewayIntents::empty()) - .event_handler(Handler { dev_guild_id }) + .event_handler(Handler { + dev_guild_id, + set_global_commands, + mongo, + }) .await .wrap_err("failed to create discord client")?; diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..5429358 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,43 @@ +use color_eyre::Result; +use mongodb::{options::ClientOptions, Client, Database}; +use serde::{Deserialize, Serialize}; +use serenity::model::id::{ChannelId, GuildId}; + +use crate::{lawsuit::Lawsuit, WrapErr}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct State { + pub guild_id: GuildId, + pub lawsuits: Vec, + pub justice_category: ChannelId, + pub court_rooms: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CourtRoom { + pub channel_id: ChannelId, + pub ongoing_lawsuit: bool, +} + +pub struct Mongo { + client: Client, + db: Database, +} + +impl Mongo { + pub async fn connect(uri: &str, db_name: &str) -> Result { + let mut client_options = ClientOptions::parse(uri) + .await + .wrap_err("failed to create client options")?; + + client_options.app_name = Some("Discord Court Bot".to_owned()); + + let client = Client::with_options(client_options).wrap_err("failed to create client")?; + + let db = client.database(db_name); + + Ok(Self { client, db }) + } + + pub fn insert_lawsuit(lawsuit: &Lawsuit) {} +}