Gateway: Simplify return value of join/join_gateway (#157)

Replaces the annoying dual-return (i.e., created `Call` *and* `Result<x>`) with a single `Return<Call/ConnectionInfo>`. Users are now informed via that a `Call` is created -- thus, cleanup in event of connection failure is now their responsibility.

Tested using `cargo make ready`.

Closes #65.
This commit is contained in:
Kyle Simpson
2023-01-09 01:35:48 +00:00
parent 5d06a429a8
commit f2fbbfeb25
8 changed files with 30 additions and 452 deletions

View File

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

View File

@@ -161,11 +161,11 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
.expect("Songbird Voice client placed in at initialisation.") .expect("Songbird Voice client placed in at initialisation.")
.clone(); .clone();
let (handler_lock, _success) = manager.join(guild_id, connect_to).await; if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
// Attach an event handler to see notifications of all track errors.
// Attach an event handler to see notifications of all track errors. let mut handler = handler_lock.lock().await;
let mut handler = handler_lock.lock().await; handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); }
Ok(()) Ok(())
} }

View File

@@ -233,11 +233,9 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
.expect("Songbird Voice client placed in at initialisation.") .expect("Songbird Voice client placed in at initialisation.")
.clone(); .clone();
let (handler_lock, success_reader) = manager.join(guild_id, connect_to).await; if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
let call_lock_for_evt = Arc::downgrade(&handler_lock);
let call_lock_for_evt = Arc::downgrade(&handler_lock);
if let Ok(_reader) = success_reader {
let mut handler = handler_lock.lock().await; let mut handler = handler_lock.lock().await;
check_msg( check_msg(
msg.channel_id msg.channel_id

View File

@@ -174,9 +174,7 @@ async fn join(ctx: &Context, msg: &Message) -> CommandResult {
.expect("Songbird Voice client placed in at initialisation.") .expect("Songbird Voice client placed in at initialisation.")
.clone(); .clone();
let (handle_lock, success) = manager.join(guild_id, connect_to).await; if let Ok(handle_lock) = manager.join(guild_id, connect_to).await {
if let Ok(_channel) = success {
check_msg( check_msg(
msg.channel_id msg.channel_id
.say(&ctx.http, &format!("Joined {}", connect_to.mention())) .say(&ctx.http, &format!("Joined {}", connect_to.mention()))

View File

@@ -253,9 +253,7 @@ async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
.expect("Songbird Voice client placed in at initialisation.") .expect("Songbird Voice client placed in at initialisation.")
.clone(); .clone();
let (handler_lock, conn_result) = manager.join(guild_id, connect_to).await; if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
if let Ok(_) = conn_result {
// NOTE: this skips listening for the actual connection result. // NOTE: this skips listening for the actual connection result.
let mut handler = handler_lock.lock().await; let mut handler = handler_lock.lock().await;

View File

@@ -131,10 +131,8 @@ async fn join(msg: Message, state: State) -> Result<(), Box<dyn Error + Send + S
let channel_id = let channel_id =
NonZeroU64::new(channel_id).ok_or("Joined voice channel must have nonzero ID.")?; NonZeroU64::new(channel_id).ok_or("Joined voice channel must have nonzero ID.")?;
let (_handle, success) = state.songbird.join(guild_id, channel_id).await; let content = match state.songbird.join(guild_id, channel_id).await {
Ok(_handle) => format!("Joined <#{}>!", channel_id),
let content = match success {
Ok(()) => format!("Joined <#{}>!", channel_id),
Err(e) => format!("Failed to join <#{}>! Why: {:?}", channel_id, e), Err(e) => format!("Failed to join <#{}>! Why: {:?}", channel_id, e),
}; };

View File

@@ -91,8 +91,7 @@ impl Display for CodecCacheError {
Self::Parse(p) => f.write_fmt(format_args!("failed to parse audio format: {p}")), Self::Parse(p) => f.write_fmt(format_args!("failed to parse audio format: {p}")),
Self::Opus(o) => f.write_fmt(format_args!("failed to create Opus encoder: {o}")), Self::Opus(o) => f.write_fmt(format_args!("failed to create Opus encoder: {o}")),
Self::MetadataEncoding(m) => f.write_fmt(format_args!( Self::MetadataEncoding(m) => f.write_fmt(format_args!(
"failed to convert track metadata to JSON: {}", "failed to convert track metadata to JSON: {m}"
m
)), )),
Self::MetadataTooLarge => f.write_str("track metadata was too large, >= 32kiB"), Self::MetadataTooLarge => f.write_str("track metadata was too large, >= 32kiB"),
Self::CreatePanicked => f.write_str("sync thread panicked while creating stream"), Self::CreatePanicked => f.write_str("sync thread panicked while creating stream"),

View File

@@ -216,11 +216,13 @@ impl Songbird {
/// ///
/// Twilight users should read the caveats mentioned in [`process`]. /// Twilight users should read the caveats mentioned in [`process`].
/// ///
/// NOTE: an `Err(..)` value will still create a [`Call`] accessible via [`get`].
///
/// [`Call`]: Call /// [`Call`]: Call
/// [`get`]: Songbird::get /// [`get`]: Songbird::get
/// [`process`]: #method.process /// [`process`]: #method.process
#[inline] #[inline]
pub async fn join<C, G>(&self, guild_id: G, channel_id: C) -> (Arc<Mutex<Call>>, JoinResult<()>) pub async fn join<C, G>(&self, guild_id: G, channel_id: C) -> JoinResult<Arc<Mutex<Call>>>
where where
C: Into<ChannelId>, C: Into<ChannelId>,
G: Into<GuildId>, G: Into<GuildId>,
@@ -233,7 +235,7 @@ impl Songbird {
&self, &self,
guild_id: GuildId, guild_id: GuildId,
channel_id: ChannelId, channel_id: ChannelId,
) -> (Arc<Mutex<Call>>, JoinResult<()>) { ) -> JoinResult<Arc<Mutex<Call>>> {
let call = self.get_or_insert(guild_id); let call = self.get_or_insert(guild_id);
let stage_1 = { let stage_1 = {
@@ -241,12 +243,10 @@ impl Songbird {
handler.join(channel_id).await handler.join(channel_id).await
}; };
let result = match stage_1 { match stage_1 {
Ok(chan) => chan.await, Ok(chan) => chan.await.map(|_| call),
Err(e) => Err(e), Err(e) => Err(e),
}; }
(call, result)
} }
/// Partially connects to a target by retrieving its relevant [`Call`] and /// Partially connects to a target by retrieving its relevant [`Call`] and
@@ -255,13 +255,16 @@ impl Songbird {
/// This method returns the handle and the connection info needed for other libraries /// This method returns the handle and the connection info needed for other libraries
/// or drivers, such as lavalink, and does not actually start or run a voice call. /// or drivers, such as lavalink, and does not actually start or run a voice call.
/// ///
/// NOTE: an `Err(..)` value will still create a [`Call`] accessible via [`get`].
///
/// [`Call`]: Call /// [`Call`]: Call
/// [`get`]: Songbird::get
#[inline] #[inline]
pub async fn join_gateway<C, G>( pub async fn join_gateway<C, G>(
&self, &self,
guild_id: G, guild_id: G,
channel_id: C, channel_id: C,
) -> (Arc<Mutex<Call>>, JoinResult<ConnectionInfo>) ) -> JoinResult<(ConnectionInfo, Arc<Mutex<Call>>)>
where where
C: Into<ChannelId>, C: Into<ChannelId>,
G: Into<GuildId>, G: Into<GuildId>,
@@ -273,7 +276,7 @@ impl Songbird {
&self, &self,
guild_id: GuildId, guild_id: GuildId,
channel_id: ChannelId, channel_id: ChannelId,
) -> (Arc<Mutex<Call>>, JoinResult<ConnectionInfo>) { ) -> JoinResult<(ConnectionInfo, Arc<Mutex<Call>>)> {
let call = self.get_or_insert(guild_id); let call = self.get_or_insert(guild_id);
let stage_1 = { let stage_1 = {
@@ -281,12 +284,13 @@ impl Songbird {
handler.join_gateway(channel_id).await handler.join_gateway(channel_id).await
}; };
let result = match stage_1 { match stage_1 {
Ok(chan) => chan.await.map_err(|_| JoinError::Dropped), Ok(chan) => chan
.await
.map_err(|_| JoinError::Dropped)
.map(|info| (info, call)),
Err(e) => Err(e), Err(e) => Err(e),
}; }
(call, result)
} }
/// Retrieves the [handler][`Call`] for the given target and leaves the /// Retrieves the [handler][`Call`] for the given target and leaves the