diff --git a/.github/workflows/scheduled_audit.yaml b/.github/workflows/scheduled_audit.yaml new file mode 100644 index 0000000..b107573 --- /dev/null +++ b/.github/workflows/scheduled_audit.yaml @@ -0,0 +1,13 @@ +name: Cargo Audit +on: + schedule: + - cron: 0 8 * * 1,3,5 # At 08:00 on Monday, Wednesday, and Friday + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0adcb6c..98eddcc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,7 @@ jobs: with: command: clippy args: -- -D warnings + format: runs-on: ubuntu-latest steps: @@ -35,6 +36,23 @@ jobs: with: command: fmt args: --all -- --check + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Rust with rustfmt + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all + build: runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml index 13b7a21..7b58ba5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,7 @@ -[package] -name = "cadency" -version = "0.2.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -env_logger = "0.9.0" -log = "0.4.14" -reqwest = "0.11.6" -thiserror = "1.0.30" - -[dependencies.serenity] -version = "0.11.5" -default-features = false -features = ["client", "gateway", "rustls_backend", "model", "voice", "cache"] - -[dependencies.tokio] -version = "1.13.0" -features = ["macros", "rt-multi-thread"] - -[dependencies.songbird] -version = "0.3.0" -features = ["builtin-queue"] - -[dependencies.serde] -version = "1.0.130" -features = ["derive"] +[workspace] +members = [ + "cadency_core", + "cadency_codegen", + "cadency_commands", + "cadency" +] diff --git a/Dockerfile b/Dockerfile index d175279..c1daf53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,6 @@ FROM debian:bullseye-slim LABEL org.opencontainers.image.source="https://github.com/jontze/cadency-rs" WORKDIR /cadency COPY --from=builder /cadency/target/release/cadency cadency -RUN apt-get update && apt-get install -y libopus-dev ffmpeg youtube-dl -ENTRYPOINT [ "./cadency" ] -CMD [ "" ] +RUN apt-get update && apt-get install -y libopus-dev ffmpeg wget python3 +RUN wget https://github.com/yt-dlp/yt-dlp/releases/download/2022.09.01/yt-dlp && chmod +x yt-dlp && mv yt-dlp /usr/bin +CMD [ "./cadency" ] diff --git a/cadency/Cargo.toml b/cadency/Cargo.toml new file mode 100644 index 0000000..4dc7dbf --- /dev/null +++ b/cadency/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cadency" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +env_logger = "0.9.0" +log = "0.4.14" + +[dependencies.cadency_core] +path = "../cadency_core" + +[dependencies.cadency_commands] +path = "../cadency_commands" + +[dependencies.tokio] +version = "1.13.0" +features = ["macros", "rt-multi-thread"] \ No newline at end of file diff --git a/cadency/src/main.rs b/cadency/src/main.rs new file mode 100644 index 0000000..1f61399 --- /dev/null +++ b/cadency/src/main.rs @@ -0,0 +1,37 @@ +#[macro_use] +extern crate log; +#[macro_use] +extern crate cadency_core; + +use cadency_commands::{ + Fib, Inspire, Now, Pause, Ping, Play, Resume, Skip, Slap, Stop, Tracks, Urban, +}; +use cadency_core::Cadency; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let commands = setup_commands![ + Fib::default(), + Inspire::default(), + Now::default(), + Pause::default(), + Ping::default(), + Play::default(), + Resume::default(), + Skip::default(), + Slap::default(), + Stop::default(), + Tracks::default(), + Urban::default(), + ]; + let mut cadency = Cadency::default() + .await + .expect("To init Cadency") + .with_commands(commands) + .await; + if let Err(why) = cadency.start().await { + error!("Client error: {:?}", why); + } +} diff --git a/cadency_codegen/Cargo.toml b/cadency_codegen/Cargo.toml new file mode 100644 index 0000000..e506b0e --- /dev/null +++ b/cadency_codegen/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cadency_codegen" +version = "0.2.1" +edition = "2021" + +[lib] +proc_macro = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +quote = "1.0.21" +syn = "1.0.99" diff --git a/cadency_codegen/src/attribute.rs b/cadency_codegen/src/attribute.rs new file mode 100644 index 0000000..1159dbb --- /dev/null +++ b/cadency_codegen/src/attribute.rs @@ -0,0 +1,26 @@ +pub(crate) mod command { + use proc_macro::TokenStream; + use syn::{ItemFn, Stmt}; + + fn log_command_usage() -> Result { + syn::parse( + quote!( + debug!("Execute {} command", self.name()); + ) + .into(), + ) + } + + fn add_start_log(function: &mut ItemFn) { + let logger = log_command_usage().expect("Failed to parse log statement"); + function.block.stmts.insert(0, logger); + } + + pub(crate) fn complete_command(mut function: ItemFn) -> TokenStream { + add_start_log(&mut function); + quote!( + #function + ) + .into() + } +} diff --git a/cadency_codegen/src/derive.rs b/cadency_codegen/src/derive.rs new file mode 100644 index 0000000..c7c205e --- /dev/null +++ b/cadency_codegen/src/derive.rs @@ -0,0 +1,23 @@ +use proc_macro::TokenStream; +use syn::DeriveInput; + +pub(crate) fn impl_command_baseline(derive_input: DeriveInput) -> TokenStream { + let struct_name = derive_input.ident; + quote! { + use cadency_core::{self, CadencyCommandBaseline}; + impl cadency_core::CadencyCommandBaseline for #struct_name { + fn name(&self) -> String { + String::from(stringify!(#struct_name)).to_lowercase() + } + + fn description(&self) -> String { + self.description.to_string() + } + + fn options(&self) -> &Vec { + self.options.as_ref() + } + } + } + .into() +} diff --git a/cadency_codegen/src/lib.rs b/cadency_codegen/src/lib.rs new file mode 100644 index 0000000..f1d6b91 --- /dev/null +++ b/cadency_codegen/src/lib.rs @@ -0,0 +1,24 @@ +#[macro_use] +extern crate quote; + +use proc_macro::TokenStream; +use syn::{parse_macro_input, DeriveInput, ItemFn}; + +mod attribute; +mod derive; + +#[proc_macro_derive(CommandBaseline)] +pub fn derive_command_baseline(input_item: TokenStream) -> TokenStream { + // Parse token stream into derive syntax tree + let tree: DeriveInput = parse_macro_input!(input_item as DeriveInput); + // Implement command trait + derive::impl_command_baseline(tree) +} + +#[proc_macro_attribute] +pub fn command(_: TokenStream, input_item: TokenStream) -> TokenStream { + // Parse function + let input_function = parse_macro_input!(input_item as ItemFn); + // Return modified function + attribute::command::complete_command(input_function) +} diff --git a/cadency_commands/Cargo.toml b/cadency_commands/Cargo.toml new file mode 100644 index 0000000..2ac2998 --- /dev/null +++ b/cadency_commands/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "cadency_commands" +version = "0.2.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.14" +reqwest = "0.11.6" + +[dependencies.serde] +version = "1.0.130" +features = ["derive"] + +[dependencies.serenity] +version = "0.11.5" +default-features = false +features = ["client", "gateway", "rustls_backend", "model", "voice", "cache"] + +[dependencies.songbird] +version = "0.3.0" +features = ["builtin-queue", "yt-dlp"] + +[dependencies.cadency_core] +path = "../cadency_core" + +[dependencies.cadency_codegen] +path = "../cadency_codegen" + +[dev-dependencies.tokio] +version = "1.13.0" +features = ["macros", "rt-multi-thread"] + +[dev-dependencies.cadency_codegen] +path = "../cadency_codegen" + diff --git a/src/commands/fib.rs b/cadency_commands/src/fib.rs similarity index 58% rename from src/commands/fib.rs rename to cadency_commands/src/fib.rs index 5b3ed0d..c3625c0 100644 --- a/src/commands/fib.rs +++ b/cadency_commands/src/fib.rs @@ -1,16 +1,32 @@ -use super::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ async_trait, client::Context, model::application::{ - command::{Command, CommandOptionType}, + command::CommandOptionType, interaction::application_command::{ApplicationCommandInteraction, CommandDataOptionValue}, }, }; -pub struct Fib; +#[derive(CommandBaseline)] +pub struct Fib { + description: &'static str, + options: Vec, +} + +impl std::default::Default for Fib { + fn default() -> Self { + Self { + description: "Calculate the nth number in the fibonacci series", + options: vec![CadencyCommandOption { + name: "number", + description: "The number in the fibonacci series", + kind: CommandOptionType::Integer, + required: true, + }], + } + } +} impl Fib { fn calc(n: &i64) -> f64 { @@ -24,30 +40,12 @@ impl Fib { #[async_trait] impl CadencyCommand for Fib { - /// Construct the slash command that will be submited to the discord api - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command - .name("fib") - .description("Calculate the nth number in the fibonacci series") - .create_option(|option| { - option - .name("number") - .description("The number in the fibonacci series") - .kind(CommandOptionType::Integer) - .required(true) - }) - }) - .await?, - ) - } - + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute fib command"); let number_option = command .data @@ -58,12 +56,12 @@ impl CadencyCommand for Fib { if let CommandDataOptionValue::Integer(fib_value) = value { Some(fib_value) } else { - error!("Fib command option not a integer: {:?}", value); + error!("{} command option not a integer: {:?}", self.name(), value); None } } None => { - error!("Fib command option empty"); + error!("{} command option empty", self.name()); None } }); diff --git a/src/commands/inspire.rs b/cadency_commands/src/inspire.rs similarity index 54% rename from src/commands/inspire.rs rename to cadency_commands/src/inspire.rs index b45107e..a0c63f9 100644 --- a/src/commands/inspire.rs +++ b/cadency_commands/src/inspire.rs @@ -1,15 +1,23 @@ -use super::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ - async_trait, - client::Context, - model::application::{ - command::Command, interaction::application_command::ApplicationCommandInteraction, - }, + async_trait, client::Context, + model::application::interaction::application_command::ApplicationCommandInteraction, }; -pub struct Inspire; +#[derive(CommandBaseline)] +pub struct Inspire { + description: &'static str, + options: Vec, +} + +impl std::default::Default for Inspire { + fn default() -> Self { + Self { + description: "Say something really inspiring!", + options: vec![], + } + } +} impl Inspire { async fn request_inspire_image_url() -> Result { @@ -23,23 +31,12 @@ impl Inspire { #[async_trait] impl CadencyCommand for Inspire { - /// Construct the slash command that will be submited to the discord api - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command - .name("inspire") - .description("Say something really inspiring!") - }) - .await?, - ) - } - + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute inspire command"); let inspire_url = Self::request_inspire_image_url().await.map_or_else( |err| { error!("{:?}", err); diff --git a/cadency_commands/src/lib.rs b/cadency_commands/src/lib.rs new file mode 100644 index 0000000..cb840d6 --- /dev/null +++ b/cadency_commands/src/lib.rs @@ -0,0 +1,79 @@ +#[macro_use] +extern crate serde; +#[macro_use] +extern crate log; +#[macro_use] +extern crate cadency_codegen; + +mod fib; +pub use fib::Fib; +mod slap; +pub use slap::Slap; +mod inspire; +pub use inspire::Inspire; +mod now; +pub use now::Now; +mod pause; +pub use pause::Pause; +mod ping; +pub use ping::Ping; +mod play; +pub use play::Play; +mod resume; +pub use resume::Resume; +mod skip; +pub use skip::Skip; +mod stop; +pub use stop::Stop; +mod tracks; +pub use tracks::Tracks; +mod urban; +pub use urban::Urban; + +#[cfg(test)] +mod test { + use cadency_core::CadencyCommandOption; + + #[test] + fn impl_commandbaseline_trait_with_macro() { + #[derive(cadency_codegen::CommandBaseline)] + struct Test { + description: String, + options: Vec, + } + assert!(true) + } + + #[test] + fn return_lowercase_struct_name_as_name() { + #[derive(cadency_codegen::CommandBaseline)] + struct Test { + description: String, + options: Vec, + } + let test = Test { + description: "123".to_string(), + options: Vec::new(), + }; + let name: String = test.name(); + assert_eq!(name, "test", "Test command name ton be lowercase {name}") + } + + #[test] + fn not_return_uppercase_struct_name_as_name() { + #[derive(cadency_codegen::CommandBaseline)] + struct Test { + description: String, + options: Vec, + } + let test = Test { + description: "123".to_string(), + options: Vec::new(), + }; + let name: String = test.name(); + assert_ne!( + name, "Test", + "Testing that the first char is not uppercase: {name}" + ) + } +} diff --git a/src/commands/now.rs b/cadency_commands/src/now.rs similarity index 65% rename from src/commands/now.rs rename to cadency_commands/src/now.rs index bac56f1..618c4f6 100644 --- a/src/commands/now.rs +++ b/cadency_commands/src/now.rs @@ -1,34 +1,34 @@ -use crate::commands::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ - async_trait, - client::Context, - model::application::{ - command::Command, interaction::application_command::ApplicationCommandInteraction, - }, + async_trait, client::Context, + model::application::interaction::application_command::ApplicationCommandInteraction, }; -pub struct Now; +#[derive(CommandBaseline)] +pub struct Now { + description: &'static str, + options: Vec, +} -#[async_trait] -impl CadencyCommand for Now { - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command.name("now").description("Show current song") - }) - .await?, - ) +impl std::default::Default for Now { + fn default() -> Self { + Self { + description: "Show current song", + options: vec![], + } } +} +#[async_trait] +impl CadencyCommand for Now { + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute now command"); if let Some(guild_id) = command.guild_id { - let manager = songbird::get(ctx).await.expect("Songbord manager"); + let manager = utils::voice::get_songbird(ctx).await; let call = manager.get(guild_id).unwrap(); let handler = call.lock().await; match handler.queue().current() { diff --git a/src/commands/pause.rs b/cadency_commands/src/pause.rs similarity index 76% rename from src/commands/pause.rs rename to cadency_commands/src/pause.rs index 636d9ac..d3e33a4 100644 --- a/src/commands/pause.rs +++ b/cadency_commands/src/pause.rs @@ -1,32 +1,32 @@ -use crate::commands::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ - async_trait, - client::Context, - model::application::{ - command::Command, interaction::application_command::ApplicationCommandInteraction, - }, + async_trait, client::Context, + model::application::interaction::application_command::ApplicationCommandInteraction, }; -pub struct Pause; +#[derive(CommandBaseline)] +pub struct Pause { + description: &'static str, + options: Vec, +} -#[async_trait] -impl CadencyCommand for Pause { - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command.name("pause").description("Pause current song") - }) - .await?, - ) +impl std::default::Default for Pause { + fn default() -> Self { + Self { + description: "Pause current song", + options: vec![], + } } +} +#[async_trait] +impl CadencyCommand for Pause { + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute pause command"); if let Some(guild_id) = command.guild_id { utils::voice::create_deferred_response(ctx, command).await?; let manager = utils::voice::get_songbird(ctx).await; diff --git a/cadency_commands/src/ping.rs b/cadency_commands/src/ping.rs new file mode 100644 index 0000000..6547938 --- /dev/null +++ b/cadency_commands/src/ping.rs @@ -0,0 +1,33 @@ +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; +use serenity::{ + async_trait, client::Context, + model::application::interaction::application_command::ApplicationCommandInteraction, +}; + +#[derive(CommandBaseline)] +pub struct Ping { + description: &'static str, + options: Vec, +} + +impl std::default::Default for Ping { + fn default() -> Self { + Self { + description: "Play Ping-Pong", + options: vec![], + } + } +} + +#[async_trait] +impl CadencyCommand for Ping { + #[command] + async fn execute<'a>( + &self, + ctx: &Context, + command: &'a mut ApplicationCommandInteraction, + ) -> Result<(), CadencyError> { + utils::create_response(ctx, command, "Pong!").await?; + Ok(()) + } +} diff --git a/src/commands/play.rs b/cadency_commands/src/play.rs similarity index 73% rename from src/commands/play.rs rename to cadency_commands/src/play.rs index 9097c85..6fa7a65 100644 --- a/src/commands/play.rs +++ b/cadency_commands/src/play.rs @@ -1,44 +1,43 @@ -use crate::commands::CadencyCommand; -use crate::error::CadencyError; -use crate::handler::voice::InactiveHandler; -use crate::utils; +use cadency_core::{ + handler::voice::InactiveHandler, utils, CadencyCommand, CadencyCommandOption, CadencyError, +}; use serenity::{ async_trait, client::Context, model::application::{ - command::{Command, CommandOptionType}, - interaction::application_command::ApplicationCommandInteraction, + command::CommandOptionType, interaction::application_command::ApplicationCommandInteraction, }, }; use songbird::events::Event; -pub struct Play; +#[derive(CommandBaseline)] +pub struct Play { + description: &'static str, + options: Vec, +} -#[async_trait] -impl CadencyCommand for Play { - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command - .name("play") - .description("Play a song from a youtube url") - .create_option(|option| { - option - .name("url") - .description("Url to the youtube audio source") - .kind(CommandOptionType::String) - .required(true) - }) - }) - .await?, - ) +impl std::default::Default for Play { + fn default() -> Self { + Self { + description: "Play a song from a youtube url", + options: vec![CadencyCommandOption { + name: "url", + description: "Url to the youtube audio source", + kind: CommandOptionType::String, + required: true, + }], + } } +} +#[async_trait] +impl CadencyCommand for Play { + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute play command"); let url_option = utils::voice::parse_valid_url(&command.data.options); if let Some(valid_url) = url_option { utils::voice::create_deferred_response(ctx, command).await?; diff --git a/src/commands/resume.rs b/cadency_commands/src/resume.rs similarity index 75% rename from src/commands/resume.rs rename to cadency_commands/src/resume.rs index 144fa58..1fe14e5 100644 --- a/src/commands/resume.rs +++ b/cadency_commands/src/resume.rs @@ -1,34 +1,32 @@ -use crate::commands::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ - async_trait, - client::Context, - model::application::{ - command::Command, interaction::application_command::ApplicationCommandInteraction, - }, + async_trait, client::Context, + model::application::interaction::application_command::ApplicationCommandInteraction, }; -pub struct Resume; +#[derive(CommandBaseline)] +pub struct Resume { + description: &'static str, + options: Vec, +} -#[async_trait] -impl CadencyCommand for Resume { - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command - .name("resume") - .description("Resume current song if paused") - }) - .await?, - ) +impl std::default::Default for Resume { + fn default() -> Self { + Self { + description: "Resume current song if paused", + options: vec![], + } } +} +#[async_trait] +impl CadencyCommand for Resume { + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute skip command"); if let Some(guild_id) = command.guild_id { utils::voice::create_deferred_response(ctx, command).await?; let manager = utils::voice::get_songbird(ctx).await; diff --git a/src/commands/skip.rs b/cadency_commands/src/skip.rs similarity index 76% rename from src/commands/skip.rs rename to cadency_commands/src/skip.rs index 5f1c0d6..18d2de6 100644 --- a/src/commands/skip.rs +++ b/cadency_commands/src/skip.rs @@ -1,32 +1,32 @@ -use crate::commands::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ - async_trait, - client::Context, - model::application::{ - command::Command, interaction::application_command::ApplicationCommandInteraction, - }, + async_trait, client::Context, + model::application::interaction::application_command::ApplicationCommandInteraction, }; -pub struct Skip; +#[derive(CommandBaseline)] +pub struct Skip { + description: &'static str, + options: Vec, +} -#[async_trait] -impl CadencyCommand for Skip { - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command.name("skip").description("Skip current song") - }) - .await?, - ) +impl std::default::Default for Skip { + fn default() -> Self { + Self { + description: "Skip current song", + options: vec![], + } } +} +#[async_trait] +impl CadencyCommand for Skip { + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute skip command"); if let Some(guild_id) = command.guild_id { utils::voice::create_deferred_response(ctx, command).await?; let manager = utils::voice::get_songbird(ctx).await; diff --git a/src/commands/slap.rs b/cadency_commands/src/slap.rs similarity index 74% rename from src/commands/slap.rs rename to cadency_commands/src/slap.rs index 8cf629b..c011999 100644 --- a/src/commands/slap.rs +++ b/cadency_commands/src/slap.rs @@ -1,42 +1,41 @@ -use super::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ async_trait, client::Context, model::application::{ - command::{Command, CommandOptionType}, + command::CommandOptionType, interaction::application_command::{ApplicationCommandInteraction, CommandDataOptionValue}, }, }; -pub struct Slap; +#[derive(CommandBaseline)] +pub struct Slap { + description: &'static str, + options: Vec, +} -#[async_trait] -impl CadencyCommand for Slap { - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command - .name("slap") - .description("Slap someone with a large trout!") - .create_option(|option| { - option - .name("target") - .description("The user you want to slap") - .kind(CommandOptionType::User) - .required(true) - }) - }) - .await?, - ) +impl std::default::Default for Slap { + fn default() -> Self { + Self { + description: "Slap someone with a large trout!", + options: vec![CadencyCommandOption { + name: "target", + description: "The user you want to slap", + kind: CommandOptionType::User, + required: true, + }], + } } +} +#[async_trait] +impl CadencyCommand for Slap { + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute slap command"); let args = command.data.options.clone(); let user_option = args .first() diff --git a/src/commands/stop.rs b/cadency_commands/src/stop.rs similarity index 69% rename from src/commands/stop.rs rename to cadency_commands/src/stop.rs index 860d6e8..65c70b2 100644 --- a/src/commands/stop.rs +++ b/cadency_commands/src/stop.rs @@ -1,34 +1,32 @@ -use crate::commands::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ - async_trait, - client::Context, - model::application::{ - command::Command, interaction::application_command::ApplicationCommandInteraction, - }, + async_trait, client::Context, + model::application::interaction::application_command::ApplicationCommandInteraction, }; -pub struct Stop; +#[derive(CommandBaseline)] +pub struct Stop { + description: &'static str, + options: Vec, +} -#[async_trait] -impl CadencyCommand for Stop { - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command - .name("stop") - .description("Stop music and clean up the track list") - }) - .await?, - ) +impl std::default::Default for Stop { + fn default() -> Self { + Self { + description: "Stop music and clean up the track list", + options: vec![], + } } +} +#[async_trait] +impl CadencyCommand for Stop { + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute stop command"); if let Some(guild_id) = command.guild_id { utils::voice::create_deferred_response(ctx, command).await?; let manager = utils::voice::get_songbird(ctx).await; diff --git a/src/commands/tracks.rs b/cadency_commands/src/tracks.rs similarity index 77% rename from src/commands/tracks.rs rename to cadency_commands/src/tracks.rs index 3485865..8ae8bf9 100644 --- a/src/commands/tracks.rs +++ b/cadency_commands/src/tracks.rs @@ -1,36 +1,33 @@ -use crate::commands::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ - async_trait, - builder::CreateEmbed, - client::Context, - model::application::{ - command::Command, interaction::application_command::ApplicationCommandInteraction, - }, + async_trait, builder::CreateEmbed, client::Context, + model::application::interaction::application_command::ApplicationCommandInteraction, utils::Color, }; -pub struct Tracks; +#[derive(CommandBaseline)] +pub struct Tracks { + description: &'static str, + options: Vec, +} -#[async_trait] -impl CadencyCommand for Tracks { - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command - .name("tracks") - .description("List all tracks in the queue") - }) - .await?, - ) +impl std::default::Default for Tracks { + fn default() -> Self { + Self { + description: "List all tracks in the queue", + options: vec![], + } } +} +#[async_trait] +impl CadencyCommand for Tracks { + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute tracks command"); if let Some(guild_id) = command.guild_id { utils::voice::create_deferred_response(ctx, command).await?; let manager = utils::voice::get_songbird(ctx).await; @@ -61,7 +58,7 @@ impl CadencyCommand for Tracks { .map_or("**No url provided**", |u| u); embeded_tracks.field( format!("{position}. :newspaper: `{title}`"), - format!(":notes: `${url}`"), + format!(":notes: `{url}`"), false, ); } diff --git a/src/commands/urban.rs b/cadency_commands/src/urban.rs similarity index 77% rename from src/commands/urban.rs rename to cadency_commands/src/urban.rs index da06ad1..f73dcaf 100644 --- a/src/commands/urban.rs +++ b/cadency_commands/src/urban.rs @@ -1,12 +1,10 @@ -use super::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; +use cadency_core::{utils, CadencyCommand, CadencyCommandOption, CadencyError}; use serenity::{ async_trait, builder::CreateEmbed, client::Context, model::application::{ - command::{Command, CommandOptionType}, + command::CommandOptionType, interaction::application_command::{ApplicationCommandInteraction, CommandDataOptionValue}, }, utils::Color, @@ -33,7 +31,25 @@ struct UrbanResult { pub list: Vec, } -pub struct Urban; +#[derive(CommandBaseline)] +pub struct Urban { + description: &'static str, + options: Vec, +} + +impl std::default::Default for Urban { + fn default() -> Self { + Self { + description: "Searches the Urbandictionary for your input", + options: vec![CadencyCommandOption { + name: "url", + description: "Your search query", + kind: CommandOptionType::String, + required: true, + }], + } + } +} impl Urban { async fn request_urban_dictionary_entries( @@ -52,18 +68,14 @@ impl Urban { } let mut embed_urban_entry = CreateEmbed::default(); embed_urban_entry.color(Color::from_rgb(255, 255, 0)); - embed_urban_entry.title(&urban.word.replace('[', "").replace(']', "")); + embed_urban_entry.title(&urban.word.replace(['[', ']'], "")); embed_urban_entry.url(&urban.permalink); embed_urban_entry.field( "Definition", - &urban.definition.replace('[', "").replace(']', ""), - false, - ); - embed_urban_entry.field( - "Example", - &urban.example.replace('[', "").replace(']', ""), + &urban.definition.replace(['[', ']'], ""), false, ); + embed_urban_entry.field("Example", &urban.example.replace(['[', ']'], ""), false); embed_urban_entry.field( "Rating", format!( @@ -80,29 +92,12 @@ impl Urban { #[async_trait] impl CadencyCommand for Urban { - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command - .name("urban") - .description("Searches the Urbandictionary for your input") - .create_option(|option| { - option - .name("query") - .description("Your search query") - .kind(CommandOptionType::String) - .required(true) - }) - }) - .await?, - ) - } - + #[command] async fn execute<'a>( + &self, ctx: &Context, command: &'a mut ApplicationCommandInteraction, ) -> Result<(), CadencyError> { - debug!("Execute urban command"); let query_option = command .data diff --git a/cadency_core/Cargo.toml b/cadency_core/Cargo.toml new file mode 100644 index 0000000..8091860 --- /dev/null +++ b/cadency_core/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "cadency_core" +version = "0.2.1" +edition = "2021" + +[dependencies] +log = "0.4.14" +reqwest = "0.11.6" +thiserror = "1.0.30" +mockall_double = "0.3.0" + +[dependencies.serenity] +version = "0.11.5" +default-features = false +features = ["client", "gateway", "rustls_backend", "model", "voice", "cache"] + +[dependencies.songbird] +version = "0.3.0" +features = ["builtin-queue", "yt-dlp"] + +[dev-dependencies] +mockall = "0.11.2" + +[dev-dependencies.tokio] +version = "1.13.0" +features = ["macros", "rt-multi-thread"] diff --git a/cadency_core/src/client.rs b/cadency_core/src/client.rs new file mode 100644 index 0000000..ac15d00 --- /dev/null +++ b/cadency_core/src/client.rs @@ -0,0 +1,106 @@ +use crate::{ + command::Commands, error::CadencyError, handler::command::Handler, intents::CadencyIntents, + CadencyCommand, +}; +use serenity::client::Client; +use songbird::SerenityInit; +#[cfg(not(test))] +use std::env; +use std::sync::Arc; + +#[cfg_attr(test, mockall::automock)] +mod env_read { + #[allow(dead_code)] + pub fn var(_env_name: &str) -> Result { + Ok(String::from("SOME_ENV_VALUE")) + } +} + +#[cfg(test)] +use mock_env_read as env; + +pub const DISCORD_TOKEN_ENV: &str = "DISCORD_TOKEN"; + +pub struct Cadency { + client: serenity::Client, +} + +#[cfg_attr(test, mockall::automock)] +impl Cadency { + /// Construct the Cadency discord but with default configuration + pub async fn default() -> Result { + let token = env::var(DISCORD_TOKEN_ENV) + .map_err(|_| CadencyError::Environment(DISCORD_TOKEN_ENV.to_string()))?; + Self::new(token).await + } + + /// Construct the Cadency discord bot with a custom token that can be set programmatically + /// + /// # Arguments + /// * `token` - The discord bot token as string + pub async fn new(token: String) -> Result { + let client = Client::builder(token, CadencyIntents::default().into()) + .event_handler(Handler) + .register_songbird() + .await + .map_err(|err| CadencyError::Builder { source: err })?; + Ok(Self { client }) + } + + /// This will register and provide the commands for cadency. + /// Every struct that implements the CadencyCommand trait can be used. + pub async fn with_commands(self, commands: Vec>) -> Self { + { + let mut data = self.client.data.write().await; + data.insert::(commands); + } + self + } + + /// This will actually start the configured Cadency bot + pub async fn start(&mut self) -> Result<(), serenity::Error> { + self.client.start().await + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::sync::{Mutex, MutexGuard}; + + static MTX: Mutex<()> = Mutex::new(()); + + // When a test panics, it will poison the Mutex. Since we don't actually + // care about the state of the data we ignore that it is poisoned and grab + // the lock regardless. If you just do `let _m = &MTX.lock().unwrap()`, one + // test panicking will cause all other tests that try and acquire a lock on + // that Mutex to also panic. + fn get_lock(m: &'static Mutex<()>) -> MutexGuard<'static, ()> { + match m.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } + } + + #[tokio::test] + async fn should_error_on_missing_env_token() { + let _m = get_lock(&MTX); + + let env_cxt = mock_env_read::var_context(); + env_cxt.expect().return_once(|_| Err(())); + let cadency = Cadency::default().await; + assert!(cadency.is_err()); + } + + #[tokio::test] + async fn should_build_cadency_with_env_token() { + let _m = get_lock(&MTX); + + let env_cxt = mock_env_read::var_context(); + env_cxt + .expect() + .return_once(|_| Ok(String::from("ENV_VAR_VALUE"))); + let test = Cadency::default().await; + assert!(test.is_ok()) + } +} diff --git a/cadency_core/src/command.rs b/cadency_core/src/command.rs new file mode 100644 index 0000000..72d5f22 --- /dev/null +++ b/cadency_core/src/command.rs @@ -0,0 +1,107 @@ +use crate::{error::CadencyError, utils}; +use serenity::{ + async_trait, + client::Context, + model::{ + application::{ + command::Command, + interaction::{ + application_command::ApplicationCommandInteraction, InteractionResponseType, + }, + }, + prelude::command::CommandOptionType, + }, + prelude::TypeMapKey, +}; +use std::sync::Arc; + +#[macro_export] +macro_rules! setup_commands { + ($($command_struct:expr),* $(,)*) => { + { + let mut commands: Vec> = Vec::new(); + $( + let command = std::sync::Arc::new($command_struct); + commands.push(command); + )* + commands + } + }; +} + +pub trait CadencyCommandBaseline { + fn name(&self) -> String; + fn description(&self) -> String; + fn options(&self) -> &Vec; +} + +pub struct CadencyCommandOption { + pub name: &'static str, + pub description: &'static str, + pub kind: CommandOptionType, + pub required: bool, +} + +#[async_trait] +pub trait CadencyCommand: Sync + Send + CadencyCommandBaseline { + /// Construct the slash command that will be submited to the discord api + async fn register(&self, ctx: &Context) -> Result { + Ok( + Command::create_global_application_command(&ctx.http, |command| { + let command_builder = command.name(self.name()).description(self.description()); + for cadency_option in self.options() { + command_builder.create_option(|option_res| { + option_res + .name(cadency_option.name) + .description(cadency_option.description) + .kind(cadency_option.kind) + .required(cadency_option.required) + }); + } + command_builder + }) + .await?, + ) + } + async fn execute<'a>( + &self, + ctx: &Context, + command: &'a mut ApplicationCommandInteraction, + ) -> Result<(), CadencyError>; +} + +pub(crate) struct Commands; + +impl TypeMapKey for Commands { + type Value = Vec>; +} + +/// Submit global slash commands to the discord api. +/// As global commands are cached for 1 hour, the activation ca take some time. +/// For local testing it is recommended to create commands with a guild scope. +pub(crate) async fn setup_commands(ctx: &Context) -> Result<(), serenity::Error> { + let commands = utils::get_commands(ctx).await; + // No need to run this in parallel as serenity will enforce one-by-one execution + for command in commands.iter() { + command.register(ctx).await?; + } + Ok(()) +} + +pub(crate) async fn command_not_implemented( + ctx: &Context, + command: ApplicationCommandInteraction, +) -> Result<(), CadencyError> { + error!("The following command is not known: {:?}", command); + command + .create_interaction_response(&ctx.http, |response| { + response + .kind(InteractionResponseType::ChannelMessageWithSource) + .interaction_response_data(|message| message.content("Unknown command")) + }) + .await + .map_err(|err| { + error!("Interaction response failed: {}", err); + CadencyError::Response + }) +} diff --git a/src/error.rs b/cadency_core/src/error.rs similarity index 52% rename from src/error.rs rename to cadency_core/src/error.rs index d71fe33..f54ead2 100644 --- a/src/error.rs +++ b/cadency_core/src/error.rs @@ -2,6 +2,10 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum CadencyError { + #[error("Missing environment variable '{0}'")] + Environment(String), + #[error("Failed to build cadency bot")] + Builder { source: serenity::Error }, #[error("Failed to join a voice channel")] Join, #[error("Response failed")] diff --git a/cadency_core/src/handler/command.rs b/cadency_core/src/handler/command.rs new file mode 100644 index 0000000..60cb485 --- /dev/null +++ b/cadency_core/src/handler/command.rs @@ -0,0 +1,48 @@ +use serenity::{ + async_trait, + client::{Context, EventHandler}, + model::{application::interaction::Interaction, event::ResumedEvent, gateway::Ready}, +}; + +use crate::{ + command::{command_not_implemented, setup_commands}, + utils, +}; + +use crate::utils::set_bot_presence; + +pub(crate) struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, ctx: Context, _data_about_bot: Ready) { + info!("🚀 Start Cadency Discord Bot"); + set_bot_presence(&ctx).await; + info!("⏳ Started to submit commands, please wait..."); + match setup_commands(&ctx).await { + Ok(_) => info!("✅ Application commands submitted"), + Err(err) => error!("❌ Failed to submit application commands: {:?}", err), + }; + } + + async fn resume(&self, _ctx: Context, _: ResumedEvent) { + debug!("🔌 Reconnect to server"); + } + + async fn interaction_create(&self, ctx: Context, interaction: Interaction) { + if let Interaction::ApplicationCommand(mut command) = interaction { + let cadency_commands = utils::get_commands(&ctx).await; + let command_name = command.data.name.as_str(); + let cmd_target = cadency_commands + .iter() + .find(|cadency_command| cadency_command.name() == command_name); + let cmd_execution = match cmd_target { + Some(target) => target.execute(&ctx, &mut command).await, + None => command_not_implemented(&ctx, command).await, + }; + if let Err(execution_err) = cmd_execution { + error!("❌ Command execution failed: {execution_err:?}"); + } + }; + } +} diff --git a/cadency_core/src/handler/mod.rs b/cadency_core/src/handler/mod.rs new file mode 100644 index 0000000..75a9734 --- /dev/null +++ b/cadency_core/src/handler/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod command; + +pub mod voice; diff --git a/src/handler/voice.rs b/cadency_core/src/handler/voice.rs similarity index 100% rename from src/handler/voice.rs rename to cadency_core/src/handler/voice.rs diff --git a/cadency_core/src/intents.rs b/cadency_core/src/intents.rs new file mode 100644 index 0000000..51b1fac --- /dev/null +++ b/cadency_core/src/intents.rs @@ -0,0 +1,42 @@ +use serenity::model::gateway::GatewayIntents; + +pub(crate) struct CadencyIntents { + inner: GatewayIntents, +} + +impl CadencyIntents { + pub fn default() -> Self { + Self { + inner: GatewayIntents::empty() + | GatewayIntents::GUILD_VOICE_STATES + | GatewayIntents::GUILDS, + } + } + + pub fn inner(&self) -> GatewayIntents { + self.inner + } +} + +impl From for GatewayIntents { + fn from(c_intents: CadencyIntents) -> Self { + c_intents.inner() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn should_have_voice_intent() { + let c_intents: GatewayIntents = CadencyIntents::default().into(); + assert!(c_intents.guild_voice_states()); + } + + #[test] + fn should_have_guild_intents() { + let c_intents: GatewayIntents = CadencyIntents::default().into(); + assert!(c_intents.guilds()); + } +} diff --git a/cadency_core/src/lib.rs b/cadency_core/src/lib.rs new file mode 100644 index 0000000..dd777ae --- /dev/null +++ b/cadency_core/src/lib.rs @@ -0,0 +1,13 @@ +#[macro_use] +extern crate log; +extern crate serenity; + +pub mod client; +pub use client::Cadency; +mod command; +pub use command::{CadencyCommand, CadencyCommandBaseline, CadencyCommandOption}; +mod error; +pub use error::CadencyError; +pub mod handler; +mod intents; +pub mod utils; diff --git a/src/utils/mod.rs b/cadency_core/src/utils/mod.rs similarity index 82% rename from src/utils/mod.rs rename to cadency_core/src/utils/mod.rs index d8f4ab5..ac04fb7 100644 --- a/src/utils/mod.rs +++ b/cadency_core/src/utils/mod.rs @@ -1,4 +1,4 @@ -use crate::error::CadencyError; +use crate::{command::Commands, error::CadencyError, CadencyCommand}; use serenity::{ builder::CreateEmbed, client::Context, @@ -10,16 +10,25 @@ use serenity::{ user::OnlineStatus, }, }; +use std::sync::Arc; pub mod voice; /// Set the online status and activity of the bot. /// Should not be set before the `ready` event. -pub async fn set_bot_presence(ctx: &Context) { +pub(crate) async fn set_bot_presence(ctx: &Context) { ctx.set_presence(Some(Activity::listening("music")), OnlineStatus::Online) .await; } +pub(crate) async fn get_commands(ctx: &Context) -> Vec> { + let data_read = ctx.data.read().await; + data_read + .get::() + .expect("Command array missing") + .clone() +} + pub async fn create_response<'a>( ctx: &Context, interaction: &mut ApplicationCommandInteraction, diff --git a/src/utils/voice.rs b/cadency_core/src/utils/voice.rs similarity index 98% rename from src/utils/voice.rs rename to cadency_core/src/utils/voice.rs index 3f5b033..31cd9dd 100644 --- a/src/utils/voice.rs +++ b/cadency_core/src/utils/voice.rs @@ -61,7 +61,7 @@ pub async fn join( } pub async fn add_song( - call: std::sync::Arc>, + call: std::sync::Arc>, url: String, ) -> Result { debug!("Add song to playlist: {}", url); diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 17b1a40..0000000 --- a/src/client.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::handler::command::Handler; -use crate::intents::CadencyIntents; -use serenity::client::{Client, ClientBuilder}; - -use songbird::SerenityInit; - -pub struct Cadency { - client: serenity::Client, -} - -impl Cadency { - /// Construct the Cadency discord but with default configuration - pub async fn default() -> Result { - let token = std::env::var("DISCORD_TOKEN").expect("Token in environment"); - Self::new(token).await - } - - /// Construct the Cadency discord but with a custom token that can be set programmatically - /// - /// # Arguments - /// * `token` - The discord bot token as string - pub async fn new(token: String) -> Result { - Ok(Self { - client: Self::create_client(token).await?, - }) - } - - /// This will actually start the configured Cadency bot - pub async fn start(&mut self) -> Result<(), serenity::Error> { - self.client.start().await - } - - /// Setup the fundamental serenity client - /// - /// # Arguments - /// * `token` - The discord bot token as string - async fn construct_client_baseline(token: String) -> ClientBuilder { - Client::builder(token, CadencyIntents::default()).event_handler(Handler) - } - - /// Create a ready to use serenity client instance with songbird audio - /// - /// # Arguments - /// * `token` - The discord bot token as string - async fn create_client(token: String) -> Result { - let client = Self::construct_client_baseline(token) - .await - .intents(CadencyIntents::default()); - client.register_songbird().await - } -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs deleted file mode 100644 index e210560..0000000 --- a/src/commands/mod.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::error::CadencyError; -use serenity::{ - async_trait, - client::Context, - model::application::{ - command::Command, - interaction::{ - application_command::ApplicationCommandInteraction, InteractionResponseType, - }, - }, -}; - -pub mod fib; -pub mod inspire; - -pub mod now; - -pub mod pause; -pub mod ping; - -pub mod play; - -pub mod resume; - -pub mod skip; -pub mod slap; - -pub mod stop; - -pub mod tracks; -pub mod urban; - -pub use fib::Fib; -pub use inspire::Inspire; - -pub use now::Now; - -pub use pause::Pause; -pub use ping::Ping; - -pub use play::Play; - -pub use resume::Resume; - -pub use skip::Skip; -pub use slap::Slap; - -pub use stop::Stop; - -pub use tracks::Tracks; -pub use urban::Urban; - -#[async_trait] -pub trait CadencyCommand { - async fn register(ctx: &Context) -> Result; - async fn execute<'a>( - ctx: &Context, - command: &'a mut ApplicationCommandInteraction, - ) -> Result<(), CadencyError>; -} - -/// Submit global slash commands to the discord api. -/// As global commands are cached for 1 hour, the activation ca take some time. -/// For local testing it is recommended to create commands with a guild scope. -pub async fn setup_commands(ctx: &Context) -> Result<(), serenity::Error> { - tokio::try_join!( - Ping::register(ctx), - Inspire::register(ctx), - Fib::register(ctx), - Urban::register(ctx), - Slap::register(ctx) - )?; - - tokio::try_join!( - Play::register(ctx), - Now::register(ctx), - Skip::register(ctx), - Pause::register(ctx), - Resume::register(ctx), - Stop::register(ctx), - Tracks::register(ctx) - )?; - Ok(()) -} - -pub async fn command_not_implemented( - ctx: &Context, - command: ApplicationCommandInteraction, -) -> Result<(), CadencyError> { - error!("The following command is not known: {:?}", command); - command - .create_interaction_response(&ctx.http, |response| { - response - .kind(InteractionResponseType::ChannelMessageWithSource) - .interaction_response_data(|message| message.content("Unknown command")) - }) - .await - .map_err(|err| { - error!("Interaction response failed: {}", err); - CadencyError::Response - }) -} diff --git a/src/commands/ping.rs b/src/commands/ping.rs deleted file mode 100644 index 6374e46..0000000 --- a/src/commands/ping.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::CadencyCommand; -use crate::error::CadencyError; -use crate::utils; -use serenity::{ - async_trait, - client::Context, - model::application::{ - command::Command, interaction::application_command::ApplicationCommandInteraction, - }, -}; - -pub struct Ping; - -#[async_trait] -impl CadencyCommand for Ping { - /// Construct the slash command that will be submited to the discord api - async fn register(ctx: &Context) -> Result { - Ok( - Command::create_global_application_command(&ctx.http, |command| { - command.name("ping").description("Play Ping-Pong") - }) - .await?, - ) - } - - async fn execute<'a>( - ctx: &Context, - command: &'a mut ApplicationCommandInteraction, - ) -> Result<(), CadencyError> { - debug!("Execute ping command"); - utils::create_response(ctx, command, "Pong!").await?; - Ok(()) - } -} diff --git a/src/handler/command.rs b/src/handler/command.rs deleted file mode 100644 index ff3f8a9..0000000 --- a/src/handler/command.rs +++ /dev/null @@ -1,61 +0,0 @@ -use serenity::{ - async_trait, - client::{Context, EventHandler}, - model::{application::interaction::Interaction, event::ResumedEvent, gateway::Ready}, -}; - -use crate::commands::{ - command_not_implemented, setup_commands, CadencyCommand, Fib, Inspire, Ping, Slap, Urban, -}; - -use crate::commands::{Now, Pause, Play, Resume, Skip, Stop, Tracks}; -use crate::utils::set_bot_presence; - -pub struct Handler; - -#[async_trait] -impl EventHandler for Handler { - async fn ready(&self, ctx: Context, _data_about_bot: Ready) { - info!("🚀 Start Cadency Discord Bot"); - set_bot_presence(&ctx).await; - info!("⏳ Started to submit commands, please wait..."); - match setup_commands(&ctx).await { - Ok(_) => info!("✅ Application commands submitted"), - Err(err) => error!("❌ Failed to submit application commands: {:?}", err), - }; - } - - async fn resume(&self, _ctx: Context, _: ResumedEvent) { - debug!("🔌 Reconnect to server"); - } - - async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - if let Interaction::ApplicationCommand(mut command) = interaction { - let cmd_execution = match command.data.name.as_str() { - "ping" => Ping::execute(&ctx, &mut command).await, - "inspire" => Inspire::execute(&ctx, &mut command).await, - "fib" => Fib::execute(&ctx, &mut command).await, - "urban" => Urban::execute(&ctx, &mut command).await, - "slap" => Slap::execute(&ctx, &mut command).await, - - "play" => Play::execute(&ctx, &mut command).await, - - "now" => Now::execute(&ctx, &mut command).await, - - "skip" => Skip::execute(&ctx, &mut command).await, - - "pause" => Pause::execute(&ctx, &mut command).await, - - "resume" => Resume::execute(&ctx, &mut command).await, - - "stop" => Stop::execute(&ctx, &mut command).await, - - "tracks" => Tracks::execute(&ctx, &mut command).await, - _ => command_not_implemented(&ctx, command).await, - }; - if let Err(execution_err) = cmd_execution { - error!("❌ Command execution failed: {:?}", execution_err); - } - }; - } -} diff --git a/src/handler/mod.rs b/src/handler/mod.rs deleted file mode 100644 index 9703540..0000000 --- a/src/handler/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod command; - -pub mod voice; diff --git a/src/intents.rs b/src/intents.rs deleted file mode 100644 index 47ac0d0..0000000 --- a/src/intents.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serenity::model::gateway::GatewayIntents; - -pub struct CadencyIntents; - -impl CadencyIntents { - pub fn default() -> GatewayIntents { - GatewayIntents::empty() | GatewayIntents::GUILD_VOICE_STATES | GatewayIntents::GUILDS - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 1667e14..0000000 --- a/src/main.rs +++ /dev/null @@ -1,26 +0,0 @@ -#[macro_use] -extern crate log; -extern crate env_logger; -extern crate reqwest; -#[macro_use] -extern crate serde; -extern crate serenity; -extern crate tokio; - -mod client; -mod commands; -mod error; -mod handler; -mod intents; -mod utils; - -use client::Cadency; - -#[tokio::main] -async fn main() { - env_logger::init(); - let mut cadency = Cadency::default().await.expect("To init Cadency"); - if let Err(why) = cadency.start().await { - error!("Client error: {:?}", why); - } -}