diff --git a/examples/README.md b/examples/README.md index 45fcf68..3dcc49e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,3 @@ # Songbird examples -These examples show more advanced use of Songbird, or how to include Songbird in bots built on other libraries, such as twilight. \ No newline at end of file +These examples show more advanced use of Songbird, or how to include Songbird in bots built on other libraries, such as twilight or serenity. diff --git a/examples/serenity/voice/Cargo.toml b/examples/serenity/voice/Cargo.toml new file mode 100644 index 0000000..0986650 --- /dev/null +++ b/examples/serenity/voice/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "voice" +version = "0.1.0" +authors = ["my name "] +edition = "2018" + +[dependencies] +tracing = "0.1" +tracing-subscriber = "0.2" +tracing-futures = "0.2" + +[dependencies.songbird] +path = "../../../" + +[dependencies.serenity] +features = ["client", "standard_framework", "voice", "rustls_backend"] +git = "https://github.com/serenity-rs/serenity" +branch = "current" + +[dependencies.tokio] +version = "0.2" +features = ["macros"] diff --git a/examples/serenity/voice/src/main.rs b/examples/serenity/voice/src/main.rs new file mode 100644 index 0000000..9ce9345 --- /dev/null +++ b/examples/serenity/voice/src/main.rs @@ -0,0 +1,291 @@ +//! 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; + +// Import the `Context` to handle commands. +use serenity::client::Context; + +use serenity::{ + async_trait, + client::{Client, EventHandler}, + framework::{ + StandardFramework, + standard::{ + Args, CommandResult, + macros::{command, group}, + }, + }, + model::{channel::Message, gateway::Ready}, + 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 framework = StandardFramework::new() + .configure(|c| c + .prefix("~")) + .group(&GENERAL_GROUP); + + let mut client = Client::builder(&token) + .event_handler(Handler) + .framework(framework) + .register_songbird() + .await + .expect("Err creating client"); + + let _ = client.start().await.map_err(|why| println!("Client ended: {:?}", why)); +} + +#[command] +#[only_in(guilds)] +async fn deafen(ctx: &Context, msg: &Message) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.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).await.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 = manager.join(guild_id, connect_to).await; + + Ok(()) +} + +#[command] +#[only_in(guilds)] +async fn leave(ctx: &Context, msg: &Message) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.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).await.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(context: &Context, msg: &Message) -> CommandResult { + check_msg(msg.channel_id.say(&context.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(()); + }, + }; + + 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).await.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 source = match songbird::ytdl(&url).await { + Ok(source) => source, + Err(why) => { + println!("Err starting source: {:?}", why); + + check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await); + + return Ok(()); + }, + }; + + handler.play_source(source); + + 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(()) +} + +#[command] +#[only_in(guilds)] +async fn undeafen(ctx: &Context, msg: &Message) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.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).await.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); + } +} diff --git a/examples/serenity/voice_events_queue/Cargo.toml b/examples/serenity/voice_events_queue/Cargo.toml new file mode 100644 index 0000000..d095e64 --- /dev/null +++ b/examples/serenity/voice_events_queue/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "voice_events_queue" +version = "0.1.0" +authors = ["my name "] +edition = "2018" + +[dependencies] +tracing = "0.1" +tracing-subscriber = "0.2" +tracing-futures = "0.2" + +[dependencies.songbird] +path = "../../../" + +[dependencies.serenity] +features = ["cache", "standard_framework", "voice", "rustls_backend"] +git = "https://github.com/serenity-rs/serenity" +branch = "current" + +[dependencies.tokio] +version = "0.2" +features = ["macros"] diff --git a/examples/serenity/voice_events_queue/src/main.rs b/examples/serenity/voice_events_queue/src/main.rs new file mode 100644 index 0000000..dcd1fb3 --- /dev/null +++ b/examples/serenity/voice_events_queue/src/main.rs @@ -0,0 +1,532 @@ +//! 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::{ + collections::HashMap, + env, + time::Duration, + sync::{atomic::{AtomicUsize, Ordering}, Arc} +}; + +use serenity::{ + async_trait, + client::{Client, Context, EventHandler}, + http::Http, + framework::{ + StandardFramework, + standard::{ + Args, CommandResult, + macros::{command, group}, + }, + }, + model::{ + channel::Message, + gateway::Ready, + misc::Mentionable, + prelude::{ChannelId, GuildId}, + }, + Result as SerenityResult, +}; + +use songbird::{ + input, + tracks::TrackQueue, + Event, + EventContext, + EventHandler as VoiceEventHandler, + SerenityInit, + TrackEvent, +}; + +// This imports `typemap`'s `Key` as `TypeMapKey`. +use serenity::prelude::*; + +struct VoiceQueueManager; + +impl TypeMapKey for VoiceQueueManager { + type Value = Arc>>; +} + +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() + .configure(|c| c + .prefix("~")) + .group(&GENERAL_GROUP); + + let mut client = Client::builder(&token) + .event_handler(Handler) + .framework(framework) + .register_songbird() + .await + .expect("Err creating client"); + + // Obtain a lock to the data owned by the client, and insert the client's + // voice manager into it. This allows the voice manager to be accessible by + // event handlers and framework commands. + { + let mut data = client.data.write().await; + data.insert::(Arc::new(Mutex::new(HashMap::new()))); + } + + let _ = client.start().await.map_err(|why| println!("Client ended: {:?}", why)); +} + +#[command] +async fn deafen(ctx: &Context, msg: &Message) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.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).await.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 (handle_lock, success) = manager.join(guild_id, connect_to).await; + + if let Ok(_channel) = success { + 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 = msg.guild(&ctx.cache).await.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).await.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_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 = msg.guild(&ctx.cache).await.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 source = match input::ytdl(&url).await { + Ok(source) => source, + Err(why) => { + println!("Err starting source: {:?}", why); + + check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await); + + return Ok(()); + }, + }; + + // This handler object will allow you to, as needed, + // control the audio track via events and further commands. + let song = handler.play_source(source.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 = msg.guild(&ctx.cache).await.unwrap(); + let guild_id = guild.id; + + let manager = songbird::get(ctx).await + .expect("Songbird Voice client placed in at initialisation.").clone(); + let queues_lock = ctx.data.read().await.get::().cloned().expect("Expected VoiceQueueManager in ShareMap."); + let mut track_queues = queues_lock.lock().await; + + if let Some(handler_lock) = manager.get(guild_id) { + let mut handler = handler_lock.lock().await; + let source = match input::ytdl(&url).await { + Ok(source) => source, + Err(why) => { + println!("Err starting source: {:?}", why); + + check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await); + + return Ok(()); + }, + }; + + // We need to ensure that this guild has a TrackQueue created for it. + let queue = track_queues.entry(guild_id) + .or_default(); + + // Queueing a track is this easy! + queue.add_source(source, &mut handler); + + check_msg(msg.channel_id.say(&ctx.http, format!("Added song to queue: position {}", 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 = msg.guild(&ctx.cache).await.unwrap(); + let guild_id = guild.id; + + let queues_lock = ctx.data.read().await.get::().cloned().expect("Expected VoiceQueueManager in ShareMap."); + let mut track_queues = queues_lock.lock().await; + + if let Some(queue) = track_queues.get_mut(&guild_id) { + 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 = msg.guild(&ctx.cache).await.unwrap(); + let guild_id = guild.id; + + let queues_lock = ctx.data.read().await.get::().cloned().expect("Expected VoiceQueueManager in ShareMap."); + let mut track_queues = queues_lock.lock().await; + + if let Some(queue) = track_queues.get_mut(&guild_id) { + let _ = 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 = msg.guild(&ctx.cache).await.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).await.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); + } +} diff --git a/examples/serenity/voice_receive/Cargo.toml b/examples/serenity/voice_receive/Cargo.toml new file mode 100644 index 0000000..172d9b1 --- /dev/null +++ b/examples/serenity/voice_receive/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "voice_receive" +version = "0.1.0" +authors = ["my name "] +edition = "2018" + +[dependencies] +env_logger = "~0.6" +log = "~0.4" + +[dependencies.songbird] +path = "../../../" + +[dependencies.serenity] +features = ["client", "standard_framework", "voice", "rustls_backend"] +git = "https://github.com/serenity-rs/serenity" +branch = "current" + +[dependencies.tokio] +version = "0.2" +features = ["macros"] diff --git a/examples/serenity/voice_receive/src/main.rs b/examples/serenity/voice_receive/src/main.rs new file mode 100644 index 0000000..f2d3791 --- /dev/null +++ b/examples/serenity/voice_receive/src/main.rs @@ -0,0 +1,281 @@ +//! 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; + +use serenity::{ + async_trait, + client::{Client, Context, EventHandler}, + framework::{ + StandardFramework, + standard::{ + macros::{command, group}, + Args, CommandResult, + }, + }, + model::{ + channel::Message, + gateway::Ready, + id::ChannelId, + misc::Mentionable + }, + Result as SerenityResult, +}; + +use songbird::{ + driver::{Config as DriverConfig, DecodeMode}, + model::payload::{ClientConnect, ClientDisconnect, Speaking}, + CoreEvent, + Event, + EventContext, + EventHandler as VoiceEventHandler, + SerenityInit, + Songbird, +}; + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, _: Context, ready: Ready) { + println!("{} is connected!", ready.user.name); + } +} + +struct Receiver; + +impl Receiver { + pub fn new() -> Self { + // You can manage state here, such as a buffer of audio packet bytes so + // you can later store them in intervals. + Self { } + } +} + +#[async_trait] +impl VoiceEventHandler for Receiver { + #[allow(unused_variables)] + async fn act(&self, ctx: &EventContext<'_>) -> Option { + use EventContext as Ctx; + match ctx { + Ctx::SpeakingStateUpdate( + Speaking {speaking, ssrc, user_id, ..} + ) => { + // Discord voice calls use RTP, where every sender uses a randomly allocated + // *Synchronisation Source* (SSRC) to allow receivers to tell which audio + // stream a received packet belongs to. As this number is not derived from + // the sender's user_id, only Discord Voice Gateway messages like this one + // inform us about which random SSRC a user has been allocated. Future voice + // packets will contain *only* the SSRC. + // + // You can implement logic here so that you can differentiate users' + // SSRCs and map the SSRC to the User ID and maintain this state. + // Using this map, you can map the `ssrc` in `voice_packet` + // to the user ID and handle their audio packets separately. + println!( + "Speaking state update: user {:?} has SSRC {:?}, using {:?}", + user_id, + ssrc, + speaking, + ); + }, + Ctx::SpeakingUpdate {ssrc, speaking} => { + // You can implement logic here which reacts to a user starting + // or stopping speaking. + println!( + "Source {} has {} speaking.", + ssrc, + if *speaking {"started"} else {"stopped"}, + ); + }, + Ctx::VoicePacket {audio, packet, payload_offset, payload_end_pad} => { + // An event which fires for every received audio packet, + // containing the decoded data. + if let Some(audio) = audio { + println!("Audio packet's first 5 samples: {:?}", audio.get(..5.min(audio.len()))); + println!( + "Audio packet sequence {:05} has {:04} bytes (decompressed from {}), SSRC {}", + packet.sequence.0, + audio.len() * std::mem::size_of::(), + packet.payload.len(), + packet.ssrc, + ); + } else { + println!("RTP packet, but no audio. Driver may not be configured to decode."); + } + }, + Ctx::RtcpPacket {packet, payload_offset, payload_end_pad} => { + // An event which fires for every received rtcp packet, + // containing the call statistics and reporting information. + println!("RTCP packet received: {:?}", packet); + }, + Ctx::ClientConnect( + ClientConnect {audio_ssrc, video_ssrc, user_id, ..} + ) => { + // You can implement your own logic here to handle a user who has joined the + // voice channel e.g., allocate structures, map their SSRC to User ID. + + println!( + "Client connected: user {:?} has audio SSRC {:?}, video SSRC {:?}", + user_id, + audio_ssrc, + video_ssrc, + ); + }, + Ctx::ClientDisconnect( + ClientDisconnect {user_id, ..} + ) => { + // You can implement your own logic here to handle a user who has left the + // voice channel e.g., finalise processing of statistics etc. + // You will typically need to map the User ID to their SSRC; observed when + // speaking or connecting. + + println!("Client disconnected: user {:?}", user_id); + }, + _ => { + // We won't be registering this struct for any more event classes. + unimplemented!() + } + } + + None + } +} + +#[group] +#[commands(join, leave, ping)] +struct General; + +#[tokio::main] +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() + .configure(|c| c + .prefix("~")) + .group(&GENERAL_GROUP); + + // Here, we need to configure Songbird to decode all incoming voice packets. + // If you want, you can do this on a per-call basis---here, we need it to + // read the audio data that other people are sending us! + let songbird = Songbird::serenity(); + songbird.set_config( + DriverConfig::default() + .decode_mode(DecodeMode::Decode) + ); + + let mut client = Client::builder(&token) + .event_handler(Handler) + .framework(framework) + .register_songbird_with(songbird.into()) + .await + .expect("Err creating client"); + + let _ = client.start().await.map_err(|why| println!("Client ended: {:?}", why)); +} + +#[command] +#[only_in(guilds)] +async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let connect_to = match args.single::() { + Ok(id) => ChannelId(id), + Err(_) => { + check_msg(msg.reply(ctx, "Requires a valid voice channel ID be given").await); + + return Ok(()); + }, + }; + + let guild = msg.guild(&ctx.cache).await.unwrap(); + let guild_id = guild.id; + + let manager = songbird::get(ctx).await + .expect("Songbird Voice client placed in at initialisation.").clone(); + + let (handler_lock, conn_result) = manager.join(guild_id, connect_to).await; + + if let Ok(_) = conn_result { + // NOTE: this skips listening for the actual connection result. + let mut handler = handler_lock.lock().await; + + handler.add_global_event( + CoreEvent::SpeakingStateUpdate.into(), + Receiver::new(), + ); + + handler.add_global_event( + CoreEvent::SpeakingUpdate.into(), + Receiver::new(), + ); + + handler.add_global_event( + CoreEvent::VoicePacket.into(), + Receiver::new(), + ); + + handler.add_global_event( + CoreEvent::RtcpPacket.into(), + Receiver::new(), + ); + + handler.add_global_event( + CoreEvent::ClientConnect.into(), + Receiver::new(), + ); + + handler.add_global_event( + CoreEvent::ClientDisconnect.into(), + Receiver::new(), + ); + + check_msg(msg.channel_id.say(&ctx.http, &format!("Joined {}", connect_to.mention())).await); + } else { + check_msg(msg.channel_id.say(&ctx.http, "Error joining the channel").await); + } + + Ok(()) +} + +#[command] +#[only_in(guilds)] +async fn leave(ctx: &Context, msg: &Message) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.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] +async fn ping(ctx: &Context, msg: &Message) -> CommandResult { + check_msg(msg.channel_id.say(&ctx.http,"Pong!").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); + } +} diff --git a/examples/serenity/voice_storage/Cargo.toml b/examples/serenity/voice_storage/Cargo.toml new file mode 100644 index 0000000..0894e73 --- /dev/null +++ b/examples/serenity/voice_storage/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "voice_storage" +version = "0.1.0" +authors = ["my name "] +edition = "2018" + +[dependencies] +tracing = "0.1" +tracing-subscriber = "0.2" +tracing-futures = "0.2" + +[dependencies.songbird] +path = "../../../" + +[dependencies.serenity] +features = ["cache", "framework", "standard_framework", "voice", "http", "rustls_backend"] +git = "https://github.com/serenity-rs/serenity" +branch = "current" + +[dependencies.tokio] +version = "0.2" +features = ["macros"] diff --git a/examples/serenity/voice_storage/Cloudkicker_-_Loops_-_22_2011_07.mp3 b/examples/serenity/voice_storage/Cloudkicker_-_Loops_-_22_2011_07.mp3 new file mode 100644 index 0000000..99b2360 Binary files /dev/null and b/examples/serenity/voice_storage/Cloudkicker_-_Loops_-_22_2011_07.mp3 differ diff --git a/examples/serenity/voice_storage/loop.wav b/examples/serenity/voice_storage/loop.wav new file mode 100644 index 0000000..53314b0 Binary files /dev/null and b/examples/serenity/voice_storage/loop.wav differ diff --git a/examples/serenity/voice_storage/src/main.rs b/examples/serenity/voice_storage/src/main.rs new file mode 100644 index 0000000..b8ee25b --- /dev/null +++ b/examples/serenity/voice_storage/src/main.rs @@ -0,0 +1,394 @@ +//! Example demonstrating how to store and convert audio streams which you +//! either want to reuse between servers, or to seek/loop on. See `join`, and `ting`. +//! +//! 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::{collections::HashMap, convert::TryInto, env, sync::Arc}; + +use serenity::{ + async_trait, + client::{Client, Context, EventHandler}, + framework::{ + StandardFramework, + standard::{ + Args, CommandResult, + macros::{command, group}, + }, + }, + model::{channel::Message, gateway::Ready, misc::Mentionable}, + prelude::Mutex, + Result as SerenityResult, +}; + +use songbird::{ + input::{ + self, + cached::{Compressed, Memory}, + Input, + }, + Bitrate, + Call, + Event, + EventContext, + EventHandler as VoiceEventHandler, + SerenityInit, + TrackEvent, +}; + +// This imports `typemap`'s `Key` as `TypeMapKey`. +use serenity::prelude::*; + +struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, _: Context, ready: Ready) { + println!("{} is connected!", ready.user.name); + } +} + +enum CachedSound { + Compressed(Compressed), + Uncompressed(Memory), +} + +impl From<&CachedSound> for Input { + fn from(obj: &CachedSound) -> Self { + use CachedSound::*; + match obj { + Compressed(c) => c.new_handle() + .into(), + Uncompressed(u) => u.new_handle() + .try_into() + .expect("Failed to create decoder for Memory source."), + } + } +} + +struct SoundStore; + +impl TypeMapKey for SoundStore { + type Value = Arc>>; +} + +#[group] +#[commands(deafen, join, leave, mute, ting, 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() + .configure(|c| c + .prefix("~")) + .group(&GENERAL_GROUP); + + let mut client = Client::builder(&token) + .event_handler(Handler) + .framework(framework) + .register_songbird() + .await + .expect("Err creating client"); + + // Obtain a lock to the data owned by the client, and insert the client's + // voice manager into it. This allows the voice manager to be accessible by + // event handlers and framework commands. + { + let mut data = client.data.write().await; + + // Loading the audio ahead of time. + let mut audio_map = HashMap::new(); + + // Creation of an in-memory source. + // + // This is a small sound effect, so storing the whole thing is relatively cheap. + // + // `spawn_loader` creates a new thread which works to copy all the audio into memory + // ahead of time. We do this in both cases to ensure optimal performance for the audio + // core. + let ting_src = Memory::new( + input::ffmpeg("ting.wav").await.expect("File should be in root folder."), + ).expect("These parameters are well-defined."); + let _ = ting_src.raw.spawn_loader(); + audio_map.insert("ting".into(), CachedSound::Uncompressed(ting_src)); + + // Another short sting, to show where each loop occurs. + let loop_src = Memory::new( + input::ffmpeg("loop.wav").await.expect("File should be in root folder."), + ).expect("These parameters are well-defined."); + let _ = loop_src.raw.spawn_loader(); + audio_map.insert("loop".into(), CachedSound::Uncompressed(loop_src)); + + // Creation of a compressed source. + // + // This is a full song, making this a much less memory-heavy choice. + // + // Music by Cloudkicker, used under CC BY-SC-SA 3.0 (https://creativecommons.org/licenses/by-nc-sa/3.0/). + let song_src = Compressed::new( + input::ffmpeg("Cloudkicker_-_Loops_-_22_2011_07.mp3").await.expect("Link may be dead."), + Bitrate::BitsPerSecond(128_000), + ).expect("These parameters are well-defined."); + let _ = song_src.raw.spawn_loader(); + audio_map.insert("song".into(), CachedSound::Compressed(song_src)); + + data.insert::(Arc::new(Mutex::new(audio_map))); + } + + let _ = client.start().await.map_err(|why| println!("Client ended: {:?}", why)); +} + +#[command] +#[only_in(guilds)] +async fn deafen(ctx: &Context, msg: &Message) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.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).await.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_reader) = manager.join(guild_id, connect_to).await; + + let call_lock_for_evt = handler_lock.clone(); + + if let Ok(_reader) = success_reader { + let mut handler = handler_lock.lock().await; + check_msg(msg.channel_id.say(&ctx.http, &format!("Joined {}", connect_to.mention())).await); + + let sources_lock = ctx.data.read().await.get::().cloned().expect("Sound cache was installed at startup."); + let sources_lock_for_evt = sources_lock.clone(); + let sources = sources_lock.lock().await; + let source = sources.get("song").expect("Handle placed into cache at startup."); + + let song = handler.play_source(source.into()); + let _ = song.set_volume(1.0); + let _ = song.enable_loop(); + + // Play a guitar chord whenever the main backing track loops. + let _ = song.add_event( + Event::Track(TrackEvent::Loop), + LoopPlaySound { + call_lock: call_lock_for_evt, + sources: sources_lock_for_evt, + }, + ); + } else { + check_msg(msg.channel_id.say(&ctx.http, "Error joining the channel").await); + } + + Ok(()) +} + +struct LoopPlaySound { + call_lock: Arc>, + sources: Arc>>, +} + +#[async_trait] +impl VoiceEventHandler for LoopPlaySound { + async fn act(&self, _ctx: &EventContext<'_>) -> Option { + let src = { + let sources = self.sources.lock().await; + sources.get("loop").expect("Handle placed into cache at startup.").into() + }; + + let mut handler = self.call_lock.lock().await; + let sound = handler.play_source(src); + let _ = sound.set_volume(0.5); + + None + } +} + +#[command] +#[only_in(guilds)] +async fn leave(ctx: &Context, msg: &Message) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.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).await.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] +#[only_in(guilds)] +async fn ting(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let guild = msg.guild(&ctx.cache).await.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 sources_lock = ctx.data.read().await.get::().cloned().expect("Sound cache was installed at startup."); + let sources = sources_lock.lock().await; + let source = sources.get("ting").expect("Handle placed into cache at startup."); + + let _sound = handler.play_source(source.into()); + + check_msg(msg.channel_id.say(&ctx.http, "Ting!").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 = msg.guild(&ctx.cache).await.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).await.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); + } +} diff --git a/examples/serenity/voice_storage/ting.wav b/examples/serenity/voice_storage/ting.wav new file mode 100644 index 0000000..4c1bd4c Binary files /dev/null and b/examples/serenity/voice_storage/ting.wav differ