Move examples from the Serenity repository

This commit is contained in:
Alex M. M
2020-11-13 16:20:57 +01:00
parent 4a897a7b76
commit f5bf54a63d
12 changed files with 1586 additions and 1 deletions

View File

@@ -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.
These examples show more advanced use of Songbird, or how to include Songbird in bots built on other libraries, such as twilight or serenity.

View File

@@ -0,0 +1,22 @@
[package]
name = "voice"
version = "0.1.0"
authors = ["my name <my@email.address>"]
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"]

View File

@@ -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::<String>() {
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<Message>) {
if let Err(why) = result {
println!("Error sending message: {:?}", why);
}
}

View File

@@ -0,0 +1,22 @@
[package]
name = "voice_events_queue"
version = "0.1.0"
authors = ["my name <my@email.address>"]
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"]

View File

@@ -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<Mutex<HashMap<GuildId, TrackQueue>>>;
}
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::<VoiceQueueManager>(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<Http>,
}
#[async_trait]
impl VoiceEventHandler for TrackEndNotifier {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
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<AtomicUsize>,
http: Arc<Http>,
}
#[async_trait]
impl VoiceEventHandler for ChannelDurationNotifier {
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
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::<String>() {
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<Http>,
}
#[async_trait]
impl VoiceEventHandler for SongFader {
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
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<Http>,
}
#[async_trait]
impl VoiceEventHandler for SongEndNotifier {
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
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::<String>() {
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::<VoiceQueueManager>().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::<VoiceQueueManager>().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::<VoiceQueueManager>().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<Message>) {
if let Err(why) = result {
println!("Error sending message: {:?}", why);
}
}

View File

@@ -0,0 +1,21 @@
[package]
name = "voice_receive"
version = "0.1.0"
authors = ["my name <my@email.address>"]
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"]

View File

@@ -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<Event> {
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::<i16>(),
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::<u64>() {
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<Message>) {
if let Err(why) = result {
println!("Error sending message: {:?}", why);
}
}

View File

@@ -0,0 +1,22 @@
[package]
name = "voice_storage"
version = "0.1.0"
authors = ["my name <my@email.address>"]
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"]

Binary file not shown.

View File

@@ -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<Mutex<HashMap<String, CachedSound>>>;
}
#[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::<SoundStore>(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::<SoundStore>().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<Mutex<Call>>,
sources: Arc<Mutex<HashMap<String, CachedSound>>>,
}
#[async_trait]
impl VoiceEventHandler for LoopPlaySound {
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
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::<SoundStore>().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<Message>) {
if let Err(why) = result {
println!("Error sending message: {:?}", why);
}
}

Binary file not shown.