diff --git a/Cargo.lock b/Cargo.lock index 100d036..663e657 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -440,6 +440,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.48", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.48", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -481,6 +516,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.7" @@ -1013,6 +1059,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1543,6 +1595,35 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +[[package]] +name = "poise" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1819d5a45e3590ef33754abce46432570c54a120798bdbf893112b4211fa09a6" +dependencies = [ + "async-trait", + "derivative", + "futures-util", + "parking_lot", + "poise_macros", + "regex", + "serenity", + "tokio", + "tracing", +] + +[[package]] +name = "poise_macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fa2c123c961e78315cd3deac7663177f12be4460f5440dbf62a7ed37b1effea" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2346,6 +2427,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -3106,6 +3193,7 @@ version = "0.1.0" dependencies = [ "dotenv", "env_logger", + "poise", "serenity", "tokio", "youmubot-cf", @@ -3209,6 +3297,7 @@ dependencies = [ "dashmap", "flume 0.10.14", "futures-util", + "poise", "reqwest", "serenity", "thiserror", diff --git a/youmubot-prelude/Cargo.toml b/youmubot-prelude/Cargo.toml index b615697..f229ca0 100644 --- a/youmubot-prelude/Cargo.toml +++ b/youmubot-prelude/Cargo.toml @@ -18,6 +18,7 @@ chrono = "0.4.19" flume = "0.10.13" dashmap = "5.3.4" thiserror = "1" +poise = "0.6" [dependencies.serenity] version = "0.12" diff --git a/youmubot-prelude/src/lib.rs b/youmubot-prelude/src/lib.rs index c6f295f..8def1eb 100644 --- a/youmubot-prelude/src/lib.rs +++ b/youmubot-prelude/src/lib.rs @@ -33,6 +33,9 @@ pub mod table_format; /// The global app data. pub type AppData = Arc>; +/// The global context type for app commands +pub type CmdContext<'a, Env> = poise::Context<'a, Env, anyhow::Error>; + /// The HTTP client. pub struct HTTPClient; diff --git a/youmubot/Cargo.toml b/youmubot/Cargo.toml index b48d8b1..7dbec7f 100644 --- a/youmubot/Cargo.toml +++ b/youmubot/Cargo.toml @@ -13,6 +13,7 @@ codeforces = ["youmubot-cf"] [dependencies] serenity = "0.12" +poise = "0.6" tokio = { version = "1.19.2", features = ["rt-multi-thread"] } dotenv = "0.15.0" env_logger = "0.9.0" diff --git a/youmubot/src/compose_framework.rs b/youmubot/src/compose_framework.rs new file mode 100644 index 0000000..77228fd --- /dev/null +++ b/youmubot/src/compose_framework.rs @@ -0,0 +1,54 @@ +use std::{future::Future, pin::Pin}; + +use serenity::{client::FullEvent, framework::Framework}; + +use youmubot_prelude::*; + +/// A Framework to compose other frameworks. +pub(crate) struct ComposedFramework { + frameworks: Box<[Box]>, +} + +impl ComposedFramework { + /// Create a new composed framework. + pub fn new(frameworks: Vec>) -> Self { + Self { + frameworks: frameworks.into_boxed_slice(), + } + } +} + +#[async_trait] +impl Framework for ComposedFramework { + async fn dispatch(&self, ctx: Context, msg: FullEvent) -> () { + if !self.frameworks.is_empty() { + self.dispatch_loop(self.frameworks.len() - 1, ctx, msg) + .await + } + } + async fn init(&mut self, client: &Client) { + for f in self.frameworks.iter_mut() { + f.init(&client).await + } + } +} +impl ComposedFramework { + /// Dispatch to all inner frameworks in a loop. Returns a `Pin>` because rust. + fn dispatch_loop<'a>( + &'a self, + index: usize, + ctx: Context, + msg: FullEvent, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + if index == 0 { + self.frameworks[index].dispatch(ctx, msg).await + } else { + self.frameworks[index] + .dispatch(ctx.clone(), msg.clone()) + .await; + self.dispatch_loop(index - 1, ctx, msg).await + } + }) + } +} diff --git a/youmubot/src/main.rs b/youmubot/src/main.rs index a719c5f..e7cff58 100644 --- a/youmubot/src/main.rs +++ b/youmubot/src/main.rs @@ -13,6 +13,10 @@ use serenity::{ use youmubot_prelude::announcer::AnnouncerHandler; use youmubot_prelude::*; +use crate::compose_framework::ComposedFramework; + +mod compose_framework; + struct Handler { hooks: Vec>>, ready_hooks: Vec CommandResult>, @@ -172,7 +176,7 @@ async fn main() { } }; - data.insert::(env); + data.insert::(env.clone()); #[cfg(feature = "core")] println!("Core enabled."); @@ -184,6 +188,41 @@ async fn main() { // Set up base framework let fw = setup_framework(&token[..]).await; + // Poise for application commands + let poise_fw = poise::Framework::builder() + .setup(|_, _, _| Box::pin(async { Ok(env) as Result<_> })) + .options(poise::FrameworkOptions { + prefix_options: poise::PrefixFrameworkOptions { + prefix: None, + mention_as_prefix: true, + execute_untracked_edits: true, + execute_self_messages: false, + ignore_thread_creation: true, + case_insensitive_commands: true, + ..Default::default() + }, + on_error: |err| { + Box::pin(async move { + if let poise::FrameworkError::Command { error, ctx, .. } = err { + let reply = format!( + "Command '{}' returned error {:?}", + ctx.invoked_command_name(), + error + ); + ctx.reply(&reply).await.pls_ok(); + println!("{}", reply) + } else { + eprintln!("Poise error: {:?}", err) + } + }) + }, + commands: vec![poise_register()], + ..Default::default() + }) + .build(); + + let composed = ComposedFramework::new(vec![Box::new(fw), Box::new(poise_fw)]); + // Sets up a client let mut client = { // Attempt to connect and set up a framework @@ -198,7 +237,7 @@ async fn main() { | GatewayIntents::DIRECT_MESSAGE_REACTIONS; Client::builder(token, intents) .type_map(data) - .framework(fw) + .framework(composed) .event_handler(handler) .await .unwrap() @@ -276,6 +315,18 @@ async fn setup_framework(token: &str) -> StandardFramework { fw } +// Poise command to register +#[poise::command( + prefix_command, + rename = "register", + required_permissions = "MANAGE_GUILD" +)] +async fn poise_register(ctx: CmdContext<'_, Env>) -> Result<()> { + // TODO: make this work for guild owners too + poise::builtins::register_application_commands_buttons(ctx).await?; + Ok(()) +} + // Hooks! #[hook]