feat: graceful shutdown, try making join and leave work (but some bug fixes are still needed)

This commit is contained in:
2026-04-08 22:18:32 -04:00
parent 288a784870
commit d2511f7a55
6 changed files with 272 additions and 97 deletions

View File

@@ -1,6 +1,6 @@
use std::sync::LazyLock;
use crate::{VCs, command::State};
use snafu::{OptionExt, Snafu};
use std::sync::LazyLock;
use twilight_model::{
application::{
command::{Command, CommandType},
@@ -17,8 +17,6 @@ use twilight_util::builder::{
InteractionResponseDataBuilder, command::CommandBuilder, embed::EmbedBuilder,
};
use crate::{VCs, command::State};
const NAME: &str = "join";
const DESCRIPTION: &str = "The bot will join the same VC as you (with intention to record)";
@@ -30,7 +28,7 @@ pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
});
#[derive(Debug, Snafu)]
enum GetGuildAndChannelIdError {
enum GetGuildAndVoiceChannelIdError {
/// this command was not used inside a guild (Discord server)
NotInGuild,
@@ -44,29 +42,11 @@ enum GetGuildAndChannelIdError {
UserNotInVC,
}
impl From<GetGuildAndChannelIdError> for Embed {
fn from(error: GetGuildAndChannelIdError) -> Embed {
match error {
GetGuildAndChannelIdError::NotInGuild => {
EmbedBuilder::new().title("Use this in a server").description("This bot can't find a VC to join if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build()
}
GetGuildAndChannelIdError::NoUser => EmbedBuilder::new().title("Not invoked by a user").description("This command works by joining the same VC as the user, but this bot didn't receive any user data. So did no user invoke it?! (This error should be impossible!)").validate().unwrap().build(),
GetGuildAndChannelIdError::NoVCsInGuild => {
EmbedBuilder::new().title("No VCs in this server").description("This bot can't find a VC to join because there aren't any in this server right now.").validate().unwrap().build()
},
GetGuildAndChannelIdError::UserNotInVC => {
EmbedBuilder::new().title("You're not in a VC").description("This bot can't follow you into VC if you aren't in one in this server.").validate().unwrap().build()
},
}
}
}
#[tracing::instrument]
fn get_guild_and_channel_id(
fn get_guild_and_voice_channel_id(
interaction: &Interaction,
vcs: &VCs,
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndChannelIdError> {
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndVoiceChannelIdError> {
let guild_id = interaction.guild_id.context(NotInGuildSnafu)?;
let user_id = interaction
@@ -77,17 +57,34 @@ fn get_guild_and_channel_id(
let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?;
let &channel_id = guild_vcs.get_left_for(&user_id).context(UserNotInVCSnafu)?;
let &voice_channel_id = guild_vcs.get_left_for(&user_id).context(UserNotInVCSnafu)?;
Ok((guild_id, channel_id))
Ok((guild_id, voice_channel_id))
}
fn get_guild_and_vc_error_to_embed(error: GetGuildAndVoiceChannelIdError) -> Embed {
match error {
GetGuildAndVoiceChannelIdError::NotInGuild => {
EmbedBuilder::new().title("Use this in a server").description("This bot can't find a VC to join if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build()
}
GetGuildAndVoiceChannelIdError::NoUser => {
EmbedBuilder::new().title("Not invoked by a user").description("This command works by joining the same VC as the user, but this bot didn't receive any user data. So did no user invoke it?! (This error should be impossible!)").validate().unwrap().build()
},
GetGuildAndVoiceChannelIdError::NoVCsInGuild => {
EmbedBuilder::new().title("No VCs in this server").description("This bot can't find a VC to join because there aren't any in this server right now.").validate().unwrap().build()
},
GetGuildAndVoiceChannelIdError::UserNotInVC => {
EmbedBuilder::new().title("You're not in a VC").description("This bot can't follow you into VC if you aren't in one in this server.").validate().unwrap().build()
},
}
}
#[tracing::instrument(skip(state))]
pub async fn handle(state: State, interaction: Interaction) {
let vcs = state.vcs;
let (guild_id, channel_id) = match get_guild_and_channel_id(&interaction, &vcs) {
Ok((guild_id, channel_id)) => (guild_id, channel_id),
let (guild_id, voice_channel_id) = match get_guild_and_voice_channel_id(&interaction, &vcs) {
Ok((guild_id, voice_channel_id)) => (guild_id, voice_channel_id),
Err(error) => {
state
.discord_client
@@ -99,7 +96,7 @@ pub async fn handle(state: State, interaction: Interaction) {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.embeds([error.into()])
.embeds([get_guild_and_vc_error_to_embed(error)])
.flags(MessageFlags::EPHEMERAL)
.build(),
),
@@ -126,24 +123,15 @@ pub async fn handle(state: State, interaction: Interaction) {
.await
.expect("TODO");
let call = loop {
tracing::error!("TODO: about to try joining");
match state.songbird.join(guild_id, channel_id).await {
Ok(call) => break call,
Err(error) => {
tracing::error!(?error, "I'm still here");
let call = state
.songbird
.join(guild_id, voice_channel_id)
.await
.expect("TODO");
if error.should_leave_server() {
state.songbird.leave(guild_id).await.expect("TODO");
} else if error.should_reconnect_driver() {
todo!();
}
}
}
};
tracing::error!(?call, "successfully joined");
let channel_mention = format!("<#{channel_id}>");
let channel_mention = format!("<#{voice_channel_id}>");
state
.discord_client
@@ -155,9 +143,4 @@ pub async fn handle(state: State, interaction: Interaction) {
]))
.await
.expect("TODO");
tracing::error!(?call, "TODO");
let call_guard = call.lock().await;
tracing::error!(?call_guard, "TODO");
}

View File

@@ -1,12 +1,23 @@
use std::sync::LazyLock;
use twilight_model::application::{
command::{Command, CommandType},
interaction::Interaction,
};
use twilight_util::builder::command::CommandBuilder;
use crate::VCs;
use crate::command::State;
use snafu::{OptionExt, Snafu};
use std::sync::LazyLock;
use twilight_model::channel::message::{Embed, MessageFlags};
use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType};
use twilight_model::id::marker::UserMarker;
use twilight_model::{
application::{
command::{Command, CommandType},
interaction::Interaction,
},
id::{
Id,
marker::{ChannelMarker, GuildMarker},
},
};
use twilight_util::builder::InteractionResponseDataBuilder;
use twilight_util::builder::command::CommandBuilder;
use twilight_util::builder::embed::EmbedBuilder;
const NAME: &str = "leave";
const DESCRIPTION: &str = "The bot will leave the VC it's in (so it won't record anyone anymore)";
@@ -18,7 +29,96 @@ pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
.build()
});
#[derive(Debug, Snafu)]
pub enum GetGuildAndVoiceChannelIdError {
/// this command was not used inside a guild (Discord server)
NotInGuild,
/// there are no voice chats in this guild
NoVCsInGuild,
/// the bot is not in a voice chat in this guild
BotNotInVC,
}
#[tracing::instrument]
pub fn get_guild_and_voice_channel_id(
bot_user_id: Id<UserMarker>,
interaction: &Interaction,
vcs: &VCs,
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndVoiceChannelIdError> {
let guild_id = interaction.guild_id.context(NotInGuildSnafu)?;
let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?;
let &voice_channel_id = guild_vcs
.get_left_for(&bot_user_id)
.context(BotNotInVCSnafu)?;
Ok((guild_id, voice_channel_id))
}
fn get_guild_and_vc_error_to_embed(error: GetGuildAndVoiceChannelIdError) -> Embed {
match error {
GetGuildAndVoiceChannelIdError::NotInGuild => {
EmbedBuilder::new().title("Use this in a server").description("This bot can't tell which VC to leave if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build()
}
GetGuildAndVoiceChannelIdError::NoVCsInGuild => {
EmbedBuilder::new().title("No VCs in this server").description("This bot can't leave VC because there aren't any in this server right now (therefore the bot must not be in any).").validate().unwrap().build()
},
GetGuildAndVoiceChannelIdError::BotNotInVC => {
EmbedBuilder::new().title("Not in a VC").description("This bot can't leave VC if it isn't in one in this server.").validate().unwrap().build()
},
}
}
#[tracing::instrument]
pub async fn handle(state: State, interaction: Interaction) {
todo!();
let (guild_id, voice_channel_id) =
match get_guild_and_voice_channel_id(state.discord_user_id, &interaction, &state.vcs) {
Ok((guild_id, voice_channel_id)) => (guild_id, voice_channel_id),
Err(error) => {
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.embeds([get_guild_and_vc_error_to_embed(error)])
.flags(MessageFlags::EPHEMERAL)
.build(),
),
},
)
.await
.expect("TODO");
return;
}
};
state.songbird.leave(guild_id).await.expect("TODO");
tracing::error!("TODO: successfully left the call");
let channel_mention = format!("<#{voice_channel_id}>");
state
.discord_client
.interaction(state.discord_application_id)
.update_response(&interaction.token)
.embeds(Some(&[EmbedBuilder::new()
.title("Left VC")
.description(format!(
"This bot left {channel_mention} (and is thereby unable to record anymore)."
))
.validate()
.unwrap()
.build()]))
.await
.expect("TODO");
}

View File

@@ -3,9 +3,10 @@ use std::{fmt::Debug, sync::Arc};
use futures::future::BoxFuture;
use patricia_tree::StringPatriciaMap;
use songbird::Songbird;
use tokio_util::sync::CancellationToken;
use twilight_model::{
application::{command::Command, interaction::Interaction},
id::{Id, marker::ApplicationMarker},
id::{Id, marker::{ApplicationMarker, UserMarker}},
};
use crate::VCs;
@@ -16,8 +17,10 @@ mod opt_out;
#[derive(Debug, Clone)]
pub struct State {
pub discord_client: Arc<twilight_http::Client>,
pub cancellation_token: CancellationToken,
pub discord_application_id: Id<ApplicationMarker>,
pub discord_client: Arc<twilight_http::Client>,
pub discord_user_id: Id<UserMarker>,
pub songbird: Arc<Songbird>,
pub vcs: Arc<VCs>,
}