//! Example demonstrating how to make use of individual track audio events, //! and how to use the `TrackQueue` system. //! //! Requires the "cache", "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 = ["cache", "framework", "standard_framework", "voice"] //! ``` use std::{ env, sync::{ atomic::{AtomicUsize, Ordering}, Arc, }, time::Duration, }; use reqwest::Client as HttpClient; use serenity::{ async_trait, client::{Client, Context, EventHandler}, framework::{ standard::{ macros::{command, group}, Args, CommandResult, }, StandardFramework, }, http::Http, model::{channel::Message, gateway::Ready, prelude::ChannelId}, prelude::{GatewayIntents, Mentionable, TypeMapKey}, Result as SerenityResult, }; use songbird::{ input::YoutubeDl, Event, EventContext, EventHandler as VoiceEventHandler, SerenityInit, TrackEvent, }; struct HttpKey; impl TypeMapKey for HttpKey { type Value = HttpClient; } 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_fade, queue, skip, stop, 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 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() .type_map_insert::(HttpClient::new()) .await .expect("Err creating client"); let _ = client .start() .await .map_err(|why| println!("Client ended: {:?}", why)); tokio::spawn(async move { let _ = client .start() .await .map_err(|why| println!("Client ended: {:?}", why)); }); let _signal_err = tokio::signal::ctrl_c().await; println!("Received Ctrl-C, shutting down."); } async fn get_http_client(ctx: &Context) -> HttpClient { let data = ctx.data.read().await; data.get::() .cloned() .expect("Guaranteed to exist in the typemap.") } #[command] async fn deafen(ctx: &Context, msg: &Message) -> CommandResult { let guild_id = msg.guild(&ctx.cache).unwrap().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_id, channel_id) = { let guild = msg.guild(&ctx.cache).unwrap(); let channel_id = guild .voice_states .get(&msg.author.id) .and_then(|voice_state| voice_state.channel_id); (guild.id, 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(); if let Ok(handle_lock) = manager.join(guild_id, connect_to).await { check_msg( msg.channel_id .say(&ctx.http, &format!("Joined {}", connect_to.mention())) .await, ); let chan_id = msg.channel_id; let send_http = ctx.http.clone(); let mut handle = handle_lock.lock().await; handle.add_global_event( Event::Track(TrackEvent::End), TrackEndNotifier { chan_id, http: send_http, }, ); let send_http = ctx.http.clone(); handle.add_global_event( Event::Periodic(Duration::from_secs(60), None), ChannelDurationNotifier { chan_id, count: Default::default(), http: send_http, }, ); } else { check_msg( msg.channel_id .say(&ctx.http, "Error joining the channel") .await, ); } Ok(()) } struct TrackEndNotifier { chan_id: ChannelId, http: Arc, } #[async_trait] impl VoiceEventHandler for TrackEndNotifier { async fn act(&self, ctx: &EventContext<'_>) -> Option { if let EventContext::Track(track_list) = ctx { check_msg( self.chan_id .say(&self.http, &format!("Tracks ended: {}.", track_list.len())) .await, ); } None } } struct ChannelDurationNotifier { chan_id: ChannelId, count: Arc, http: Arc, } #[async_trait] impl VoiceEventHandler for ChannelDurationNotifier { async fn act(&self, _ctx: &EventContext<'_>) -> Option { let count_before = self.count.fetch_add(1, Ordering::Relaxed); check_msg( self.chan_id .say( &self.http, &format!( "I've been in this channel for {} minutes!", count_before + 1 ), ) .await, ); None } } #[command] #[only_in(guilds)] async fn leave(ctx: &Context, msg: &Message) -> CommandResult { let guild_id = msg.guild(&ctx.cache).unwrap().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_id = msg.guild(&ctx.cache).unwrap().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_fade(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(()); }, }; if !url.starts_with("http") { check_msg( msg.channel_id .say(&ctx.http, "Must provide a valid URL") .await, ); return Ok(()); } let guild_id = msg.guild_id.unwrap(); let http_client = get_http_client(ctx).await; 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 src = YoutubeDl::new(http_client, url); // This handler object will allow you to, as needed, // control the audio track via events and further commands. let song = handler.play_input(src.into()); let send_http = ctx.http.clone(); let chan_id = msg.channel_id; // This shows how to periodically fire an event, in this case to // periodically make a track quieter until it can be no longer heard. let _ = song.add_event( Event::Periodic(Duration::from_secs(5), Some(Duration::from_secs(7))), SongFader { chan_id, http: send_http, }, ); let send_http = ctx.http.clone(); // This shows how to fire an event once an audio track completes, // either due to hitting the end of the bytestream or stopped by user code. let _ = song.add_event( Event::Track(TrackEvent::End), SongEndNotifier { chan_id, http: send_http, }, ); 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 SongFader { chan_id: ChannelId, http: Arc, } #[async_trait] impl VoiceEventHandler for SongFader { async fn act(&self, ctx: &EventContext<'_>) -> Option { if let EventContext::Track(&[(state, track)]) = ctx { let _ = track.set_volume(state.volume / 2.0); if state.volume < 1e-2 { let _ = track.stop(); check_msg(self.chan_id.say(&self.http, "Stopping song...").await); Some(Event::Cancel) } else { check_msg(self.chan_id.say(&self.http, "Volume reduced.").await); None } } else { None } } } struct SongEndNotifier { chan_id: ChannelId, http: Arc, } #[async_trait] impl VoiceEventHandler for SongEndNotifier { async fn act(&self, _ctx: &EventContext<'_>) -> Option { check_msg( self.chan_id .say(&self.http, "Song faded out completely!") .await, ); None } } #[command] #[only_in(guilds)] async fn queue(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(()); }, }; if !url.starts_with("http") { check_msg( msg.channel_id .say(&ctx.http, "Must provide a valid URL") .await, ); return Ok(()); } let guild_id = msg.guild_id.unwrap(); let http_client = get_http_client(ctx).await; 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; // Here, we use lazy restartable sources to make sure that we don't pay // for decoding, playback on tracks which aren't actually live yet. let src = YoutubeDl::new(http_client, url); handler.enqueue_input(src.into()).await; check_msg( msg.channel_id .say( &ctx.http, format!("Added song to queue: position {}", handler.queue().len()), ) .await, ); } else { check_msg( msg.channel_id .say(&ctx.http, "Not in a voice channel to play in") .await, ); } Ok(()) } #[command] #[only_in(guilds)] async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let guild_id = msg.guild_id.unwrap(); 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 handler = handler_lock.lock().await; let queue = handler.queue(); let _ = queue.skip(); check_msg( msg.channel_id .say( &ctx.http, format!("Song skipped: {} in queue.", queue.len()), ) .await, ); } else { check_msg( msg.channel_id .say(&ctx.http, "Not in a voice channel to play in") .await, ); } Ok(()) } #[command] #[only_in(guilds)] async fn stop(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let guild_id = msg.guild_id.unwrap(); 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 handler = handler_lock.lock().await; let queue = handler.queue(); queue.stop(); check_msg(msg.channel_id.say(&ctx.http, "Queue cleared.").await); } else { check_msg( msg.channel_id .say(&ctx.http, "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(); 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_id = msg.guild_id.unwrap(); 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); } }