Takes the chance to bring shared deps into the example workspace while we're here. Release notes, tags etc will follow in time.
655 lines
17 KiB
Rust
655 lines
17 KiB
Rust
//! 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,
|
|
Configuration,
|
|
},
|
|
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(Configuration::new().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::<HttpKey>(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::<HttpKey>()
|
|
.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<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_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::<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_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<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_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<Message>) {
|
|
if let Err(why) = result {
|
|
println!("Error sending message: {:?}", why);
|
|
}
|
|
}
|