From 0010e6d83ffc0c633d8d2e97b658dac0656b44a7 Mon Sep 17 00:00:00 2001 From: GnomedDev Date: Tue, 2 Apr 2024 15:06:19 +0100 Subject: [PATCH] Rewrite basic voice example to use poise --- examples/serenity/voice/Cargo.toml | 1 + examples/serenity/voice/src/main.rs | 294 ++++++++++------------------ 2 files changed, 103 insertions(+), 192 deletions(-) diff --git a/examples/serenity/voice/Cargo.toml b/examples/serenity/voice/Cargo.toml index 656747371..4dd3bf201 100644 --- a/examples/serenity/voice/Cargo.toml +++ b/examples/serenity/voice/Cargo.toml @@ -13,3 +13,4 @@ tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } tracing-futures = { workspace = true } +poise = { version = "0.6.1", default-features = false, features = ["cache"] } diff --git a/examples/serenity/voice/src/main.rs b/examples/serenity/voice/src/main.rs index 8059b7f2c..f9efe712b 100644 --- a/examples/serenity/voice/src/main.rs +++ b/examples/serenity/voice/src/main.rs @@ -7,11 +7,9 @@ //! features = ["client", "standard_framework", "voice"] //! ``` use std::env; +use std::sync::Arc; -// This trait adds the `register_songbird` and `register_songbird_with` methods -// to the client builder below, making it easy to install this voice client. -// The voice client can be retrieved in any command using `songbird::get(ctx).await`. -use songbird::SerenityInit; +use serenity::all as serenity; // Event related imports to detect track creation failures. use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler, TrackEvent}; @@ -22,45 +20,24 @@ use songbird::input::YoutubeDl; // YtDl requests need an HTTP client to operate -- we'll create and store our own. use reqwest::Client as HttpClient; -// Import the `Context` to handle commands. -use serenity::client::Context; - -use serenity::{ - async_trait, - client::{Client, EventHandler}, - framework::{ - standard::{ - macros::{command, group}, - Args, - CommandResult, - Configuration, - }, - StandardFramework, - }, - model::{channel::Message, gateway::Ready}, - prelude::{GatewayIntents, TypeMapKey}, - Result as SerenityResult, -}; - -struct HttpKey; - -impl TypeMapKey for HttpKey { - type Value = HttpClient; +struct UserData { + http: HttpClient, + songbird: Arc, } +type Error = Box; +type Context<'a> = poise::Context<'a, UserData, Error>; +type CommandResult = Result<(), Error>; + struct Handler; -#[async_trait] -impl EventHandler for Handler { - async fn ready(&self, _: Context, ready: Ready) { +#[serenity::async_trait] +impl serenity::EventHandler for Handler { + async fn ready(&self, _: serenity::Context, ready: serenity::Ready) { println!("{} is connected!", ready.user.name); } } -#[group] -#[commands(deafen, join, leave, mute, play, ping, undeafen, unmute)] -struct General; - #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); @@ -68,21 +45,41 @@ async fn main() { // Configure the client with your Discord bot token in the environment. let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - let framework = StandardFramework::new().group(&GENERAL_GROUP); - framework.configure(Configuration::new().prefix("~")); + // Create our songbird voice manager + let manager = songbird::Songbird::serenity(); + + // Configure our command framework + let options = poise::FrameworkOptions { + commands: vec![deafen(), undeafen(), join(), leave(), play()], + prefix_options: poise::PrefixFrameworkOptions { + prefix: Some(String::from("~")), + ..Default::default() + }, + ..Default::default() + }; - let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; + // We have to clone our voice manager's Arc to share it between serenity and our user data. + let manager_clone = Arc::clone(&manager); + let framework = poise::Framework::new(options, |_, _, _| { + Box::pin(async { + Ok( + // We create a global HTTP client here to make use of in + // `~play`. If we wanted, we could supply cookies and auth + // details ahead of time. + UserData { + http: HttpClient::new(), + songbird: manager_clone, + }, + ) + }) + }); - let mut client = Client::builder(&token, intents) + let intents = + serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; + let mut client = serenity::Client::builder(&token, intents) + .voice_manager_arc(manager) .event_handler(Handler) .framework(framework) - .register_songbird() - // We insert our own HTTP client here to make use of in - // `~play`. If we wanted, we could supply cookies and auth - // details ahead of time. - // - // Generally, we don't want to make a new Client for every request! - .type_map_insert::(HttpClient::new()) .await .expect("Err creating client"); @@ -97,20 +94,15 @@ async fn main() { println!("Received Ctrl-C, shutting down."); } -#[command] -#[only_in(guilds)] -async fn deafen(ctx: &Context, msg: &Message) -> CommandResult { - let guild_id = msg.guild_id.unwrap(); - - let manager = songbird::get(ctx) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); +#[poise::command(prefix_command, guild_only)] +async fn deafen(ctx: Context<'_>) -> CommandResult { + let guild_id = ctx.guild_id().unwrap(); + let manager = &ctx.data().songbird; let handler_lock = match manager.get(guild_id) { Some(handler) => handler, None => { - check_msg(msg.reply(ctx, "Not in a voice channel").await); + check_msg(ctx.reply("Not in a voice channel").await); return Ok(()); }, @@ -119,30 +111,25 @@ async fn deafen(ctx: &Context, msg: &Message) -> CommandResult { let mut handler = handler_lock.lock().await; if handler.is_deaf() { - check_msg(msg.channel_id.say(&ctx.http, "Already deafened").await); + check_msg(ctx.say("Already deafened").await); } else { if let Err(e) = handler.deafen(true).await { - check_msg( - msg.channel_id - .say(&ctx.http, format!("Failed: {:?}", e)) - .await, - ); + check_msg(ctx.say(format!("Failed: {:?}", e)).await); } - check_msg(msg.channel_id.say(&ctx.http, "Deafened").await); + check_msg(ctx.say("Deafened").await); } Ok(()) } -#[command] -#[only_in(guilds)] -async fn join(ctx: &Context, msg: &Message) -> CommandResult { +#[poise::command(prefix_command, guild_only)] +async fn join(ctx: Context<'_>) -> CommandResult { let (guild_id, channel_id) = { - let guild = msg.guild(&ctx.cache).unwrap(); + let guild = ctx.guild().unwrap(); let channel_id = guild .voice_states - .get(&msg.author.id) + .get(&ctx.author().id) .and_then(|voice_state| voice_state.channel_id); (guild.id, channel_id) @@ -151,17 +138,12 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult { let connect_to = match channel_id { Some(channel) => channel, None => { - check_msg(msg.reply(ctx, "Not in a voice channel").await); - + check_msg(ctx.reply("Not in a voice channel").await); return Ok(()); }, }; - let manager = songbird::get(ctx) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); - + let manager = &ctx.data().songbird; if let Ok(handler_lock) = manager.join(guild_id, connect_to).await { // Attach an event handler to see notifications of all track errors. let mut handler = handler_lock.lock().await; @@ -173,7 +155,7 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult { struct TrackErrorNotifier; -#[async_trait] +#[serenity::async_trait] impl VoiceEventHandler for TrackErrorNotifier { async fn act(&self, ctx: &EventContext<'_>) -> Option { if let EventContext::Track(track_list) = ctx { @@ -190,48 +172,35 @@ impl VoiceEventHandler for TrackErrorNotifier { } } -#[command] -#[only_in(guilds)] -async fn leave(ctx: &Context, msg: &Message) -> CommandResult { - let guild_id = msg.guild_id.unwrap(); +#[poise::command(prefix_command, guild_only)] +async fn leave(ctx: Context<'_>) -> CommandResult { + let guild_id = ctx.guild_id().unwrap(); - let manager = songbird::get(ctx) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); + let manager = &ctx.data().songbird; let has_handler = manager.get(guild_id).is_some(); if has_handler { if let Err(e) = manager.remove(guild_id).await { - check_msg( - msg.channel_id - .say(&ctx.http, format!("Failed: {:?}", e)) - .await, - ); + check_msg(ctx.say(format!("Failed: {:?}", e)).await); } - check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await); + check_msg(ctx.say("Left voice channel").await); } else { - check_msg(msg.reply(ctx, "Not in a voice channel").await); + check_msg(ctx.reply("Not in a voice channel").await); } Ok(()) } -#[command] -#[only_in(guilds)] -async fn mute(ctx: &Context, msg: &Message) -> CommandResult { - let guild_id = msg.guild_id.unwrap(); - - let manager = songbird::get(ctx) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); +#[poise::command(prefix_command, guild_only)] +async fn mute(ctx: Context<'_>) -> CommandResult { + let guild_id = ctx.guild_id().unwrap(); + let manager = &ctx.data().songbird; let handler_lock = match manager.get(guild_id) { Some(handler) => handler, None => { - check_msg(msg.reply(ctx, "Not in a voice channel").await); + check_msg(ctx.reply("Not in a voice channel").await); return Ok(()); }, @@ -240,148 +209,89 @@ async fn mute(ctx: &Context, msg: &Message) -> CommandResult { let mut handler = handler_lock.lock().await; if handler.is_mute() { - check_msg(msg.channel_id.say(&ctx.http, "Already muted").await); + check_msg(ctx.say("Already muted").await); } else { if let Err(e) = handler.mute(true).await { - check_msg( - msg.channel_id - .say(&ctx.http, format!("Failed: {:?}", e)) - .await, - ); + check_msg(ctx.say(format!("Failed: {:?}", e)).await); } - check_msg(msg.channel_id.say(&ctx.http, "Now muted").await); + check_msg(ctx.say("Now muted").await); } Ok(()) } -#[command] -async fn ping(ctx: &Context, msg: &Message) -> CommandResult { - check_msg(msg.channel_id.say(&ctx.http, "Pong!").await); +#[poise::command(prefix_command, guild_only)] +async fn ping(ctx: Context<'_>) -> CommandResult { + check_msg(ctx.say("Pong!").await); Ok(()) } -#[command] -#[only_in(guilds)] -async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let url = match args.single::() { - Ok(url) => url, - Err(_) => { - check_msg( - msg.channel_id - .say(&ctx.http, "Must provide a URL to a video or audio") - .await, - ); - - return Ok(()); - }, - }; - +#[poise::command(prefix_command, guild_only)] +async fn play(ctx: Context<'_>, url: String) -> CommandResult { let do_search = !url.starts_with("http"); - let guild_id = msg.guild_id.unwrap(); - - let http_client = { - let data = ctx.data.read().await; - data.get::() - .cloned() - .expect("Guaranteed to exist in the typemap.") - }; + let guild_id = ctx.guild_id().unwrap(); + let data = ctx.data(); - let manager = songbird::get(ctx) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); - - if let Some(handler_lock) = manager.get(guild_id) { + if let Some(handler_lock) = data.songbird.get(guild_id) { let mut handler = handler_lock.lock().await; - let mut src = if do_search { - YoutubeDl::new_search(http_client, url) + let src = if do_search { + YoutubeDl::new_search(data.http.clone(), url) } else { - YoutubeDl::new(http_client, url) + YoutubeDl::new(data.http.clone(), url) }; - let _ = handler.play_input(src.clone().into()); + let _ = handler.play_input(src.into()); - check_msg(msg.channel_id.say(&ctx.http, "Playing song").await); + check_msg(ctx.say("Playing song").await); } else { - check_msg( - msg.channel_id - .say(&ctx.http, "Not in a voice channel to play in") - .await, - ); + check_msg(ctx.say("Not in a voice channel to play in").await); } Ok(()) } -#[command] -#[only_in(guilds)] -async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult { - let guild_id = msg.guild_id.unwrap(); - - let manager = songbird::get(ctx) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); +#[poise::command(prefix_command, guild_only)] +async fn undeafen(ctx: Context<'_>) -> CommandResult { + let guild_id = ctx.guild_id().unwrap(); + let manager = &ctx.data().songbird; if let Some(handler_lock) = manager.get(guild_id) { let mut handler = handler_lock.lock().await; if let Err(e) = handler.deafen(false).await { - check_msg( - msg.channel_id - .say(&ctx.http, format!("Failed: {:?}", e)) - .await, - ); + check_msg(ctx.say(format!("Failed: {:?}", e)).await); } - check_msg(msg.channel_id.say(&ctx.http, "Undeafened").await); + check_msg(ctx.say("Undeafened").await); } else { - check_msg( - msg.channel_id - .say(&ctx.http, "Not in a voice channel to undeafen in") - .await, - ); + check_msg(ctx.say("Not in a voice channel to undeafen in").await); } Ok(()) } -#[command] -#[only_in(guilds)] -async fn unmute(ctx: &Context, msg: &Message) -> CommandResult { - let guild_id = msg.guild_id.unwrap(); - - let manager = songbird::get(ctx) - .await - .expect("Songbird Voice client placed in at initialisation.") - .clone(); +#[poise::command(prefix_command, guild_only)] +async fn unmute(ctx: Context<'_>) -> CommandResult { + let guild_id = ctx.guild_id().unwrap(); + let manager = &ctx.data().songbird; if let Some(handler_lock) = manager.get(guild_id) { let mut handler = handler_lock.lock().await; if let Err(e) = handler.mute(false).await { - check_msg( - msg.channel_id - .say(&ctx.http, format!("Failed: {:?}", e)) - .await, - ); + check_msg(ctx.say(format!("Failed: {:?}", e)).await); } - check_msg(msg.channel_id.say(&ctx.http, "Unmuted").await); + check_msg(ctx.say("Unmuted").await); } else { - check_msg( - msg.channel_id - .say(&ctx.http, "Not in a voice channel to unmute in") - .await, - ); + check_msg(ctx.say("Not in a voice channel to unmute in").await); } Ok(()) } /// Checks that a message successfully sent; if not, then logs why to stdout. -fn check_msg(result: SerenityResult) { +fn check_msg(result: serenity::Result) { if let Err(why) = result { println!("Error sending message: {:?}", why); }