//! Requires the "client", "standard_framework", and "voice" features be enabled in your //! Cargo.toml, like so: //! //! ```toml //! [dependencies.serenity] //! git = "https://github.com/serenity-rs/serenity.git" //! features = ["client", "standard_framework", "voice"] //! ``` use std::env; // 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; // Event related imports to detect track creation failures. use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler, TrackEvent}; use std::time::Duration; // Import the `Context` to handle commands. use serenity::client::Context; use serenity::{ async_trait, client::{Client, EventHandler}, framework::{ standard::{ macros::{command, group}, Args, CommandResult, }, StandardFramework, }, model::{channel::Message, gateway::Ready}, prelude::GatewayIntents, Result as SerenityResult, }; struct Handler; #[async_trait] impl EventHandler for Handler { async fn ready(&self, _: Context, ready: 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(); // 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 mut framework = StandardFramework::new() .group(&GENERAL_GROUP); framework.configure(|c| c.prefix("~")); let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; let mut client = Client::builder(&token, intents) .event_handler(Handler) .framework(framework) .register_songbird() .await .expect("Err creating client"); tokio::spawn(async move { let _ = client .start() .await .map_err(|why| println!("Client ended: {:?}", why)); }); tokio::signal::ctrl_c().await; println!("Received Ctrl-C, shutting down."); } #[command] #[only_in(guilds)] async fn deafen(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).unwrap(); let guild_id = guild.id; let manager = songbird::get(ctx) .await .expect("Songbird Voice client placed in at initialisation.") .clone(); let handler_lock = match manager.get(guild_id) { Some(handler) => handler, None => { check_msg(msg.reply(ctx, "Not in a voice channel").await); return Ok(()); }, }; let mut handler = handler_lock.lock().await; if handler.is_deaf() { check_msg(msg.channel_id.say(&ctx.http, "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(msg.channel_id.say(&ctx.http, "Deafened").await); } Ok(()) } #[command] #[only_in(guilds)] async fn join(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).unwrap(); let guild_id = guild.id; let channel_id = guild .voice_states .get(&msg.author.id) .and_then(|voice_state| voice_state.channel_id); let connect_to = match channel_id { Some(channel) => channel, None => { check_msg(msg.reply(ctx, "Not in a voice channel").await); return Ok(()); }, }; let manager = songbird::get(ctx) .await .expect("Songbird Voice client placed in at initialisation.") .clone(); let (handler_lock, _success) = 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; handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); Ok(()) } struct TrackErrorNotifier; #[async_trait] impl VoiceEventHandler for TrackErrorNotifier { async fn act(&self, ctx: &EventContext<'_>) -> Option { if let EventContext::Track(track_list) = ctx { for (state, handle) in *track_list { println!( "Track {:?} encountered an error: {:?}", handle.uuid(), state.playing ); } } None } } #[command] #[only_in(guilds)] async fn leave(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).unwrap(); let guild_id = guild.id; let manager = songbird::get(ctx) .await .expect("Songbird Voice client placed in at initialisation.") .clone(); 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(msg.channel_id.say(&ctx.http, "Left voice channel").await); } else { check_msg(msg.reply(ctx, "Not in a voice channel").await); } Ok(()) } #[command] #[only_in(guilds)] async fn mute(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).unwrap(); let guild_id = guild.id; let manager = songbird::get(ctx) .await .expect("Songbird Voice client placed in at initialisation.") .clone(); let handler_lock = match manager.get(guild_id) { Some(handler) => handler, None => { check_msg(msg.reply(ctx, "Not in a voice channel").await); return Ok(()); }, }; let mut handler = handler_lock.lock().await; if handler.is_mute() { check_msg(msg.channel_id.say(&ctx.http, "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(msg.channel_id.say(&ctx.http, "Now muted").await); } Ok(()) } #[command] async fn ping(ctx: &Context, msg: &Message) -> CommandResult { check_msg(msg.channel_id.say(&ctx.http, "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(()); }, }; let time = match args.single::() { Ok(t) => t, Err(_) => { check_msg( msg.channel_id .say(&ctx.http, "Must provide a valid f64 timestamp to jump to.") .await, ); return Ok(()); }, }; if !url.starts_with("http") { check_msg( msg.channel_id .say(&ctx.http, "Must provide a valid URL") .await, ); return Ok(()); } let guild = msg.guild(&ctx.cache).unwrap(); let guild_id = guild.id; let manager = songbird::get(ctx) .await .expect("Songbird Voice client placed in at initialisation.") .clone(); if let Some(handler_lock) = manager.get(guild_id) { let mut handler = handler_lock.lock().await; let jar = reqwest::cookie::Jar::default(); jar.add_cookie_str("CONSENT=YES+; Path=/; Domain=youtube.com; Secure; Expires=Fri, 01 Jan 2038 00:00:00 GMT;", &"https://youtube.com".parse().unwrap()); let src = songbird::input::YoutubeDl::new_ytdl_like("yt-dlp", reqwest::Client::builder().cookie_provider(std::sync::Arc::new(jar)).build().unwrap(), url); let h = handler.play_input(src.into()); h.add_event( Event::Delayed(Duration::from_secs(10)), SkipHandler { skip_to: Duration::from_secs_f64(time), }, ); check_msg(msg.channel_id.say(&ctx.http, "Playing song").await); } else { check_msg( msg.channel_id .say(&ctx.http, "Not in a voice channel to play in") .await, ); } Ok(()) } struct SkipHandler { skip_to: Duration, } #[async_trait] impl VoiceEventHandler for SkipHandler { async fn act(&self, ctx: &EventContext<'_>) -> Option { if let EventContext::Track(&[(state, handle)]) = ctx { println!("Current position is {:?}", state.position); println!("Seeking to {:?}", self.skip_to); let resp = handle.seek_time(self.skip_to); println!("Seek response {:?}", resp); return None; } println!("This wasn't supposed to happen."); None } } #[command] #[only_in(guilds)] async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).unwrap(); let guild_id = guild.id; let manager = songbird::get(ctx) .await .expect("Songbird Voice client placed in at initialisation.") .clone(); 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(msg.channel_id.say(&ctx.http, "Undeafened").await); } else { check_msg( msg.channel_id .say(&ctx.http, "Not in a voice channel to undeafen in") .await, ); } Ok(()) } #[command] #[only_in(guilds)] async fn unmute(ctx: &Context, msg: &Message) -> CommandResult { let guild = msg.guild(&ctx.cache).unwrap(); let guild_id = guild.id; let manager = songbird::get(ctx) .await .expect("Songbird Voice client placed in at initialisation.") .clone(); 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(msg.channel_id.say(&ctx.http, "Unmuted").await); } else { check_msg( msg.channel_id .say(&ctx.http, "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) { if let Err(why) = result { println!("Error sending message: {:?}", why); } }