feat: improve transparency of the join command

This commit is contained in:
2026-04-07 00:24:27 -04:00
parent 663586a65c
commit 3897d1deb2
3 changed files with 225 additions and 109 deletions

View File

@@ -1,12 +1,23 @@
use std::sync::LazyLock;
use twilight_model::application::{
use snafu::{OptionExt, Snafu};
use twilight_model::{
application::{
command::{Command, CommandType},
interaction::{Interaction, application_command::CommandData},
interaction::Interaction,
},
channel::message::{Embed, MessageFlags},
http::interaction::{InteractionResponse, InteractionResponseType},
id::{
Id,
marker::{ChannelMarker, GuildMarker},
},
};
use twilight_util::builder::{
InteractionResponseDataBuilder, command::CommandBuilder, embed::EmbedBuilder,
};
use twilight_util::builder::command::CommandBuilder;
use crate::command::State;
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)";
@@ -18,23 +29,102 @@ pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
.build()
});
#[derive(Debug, Snafu)]
enum GetGuildAndChannelIdError {
/// this command was not used inside a guild (Discord server)
NotInGuild,
/// there is no user who invoked this command
NoUser,
/// there are no voice chats in this guild
NoVCsInGuild,
/// the user is not in a voice chat in this guild
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(
interaction: &Interaction,
vcs: &VCs,
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>), GetGuildAndChannelIdError> {
let guild_id = interaction.guild_id.context(NotInGuildSnafu)?;
let user_id = interaction
.member
.as_ref()
.and_then(|member| member.user.as_ref().map(|user| user.id))
.context(NoUserSnafu)?;
let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?;
let &channel_id = guild_vcs.get_left_for(&user_id).context(UserNotInVCSnafu)?;
Ok((guild_id, channel_id))
}
#[tracing::instrument]
pub async fn handle(state: State, interaction: Interaction) {
let vcs = state.vcs;
let guild_id = interaction.guild_id.expect("TODO");
// let user_id = data.user.map(|user| user.id).expect("TODO");
let user_id = interaction
.member
.and_then(|member| member.user.map(|user| user.id))
let (guild_id, channel_id) = match get_guild_and_channel_id(&interaction, &vcs) {
Ok((guild_id, channel_id)) => (guild_id, 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([error.into()])
.flags(MessageFlags::EPHEMERAL)
.build(),
),
},
)
.await
.expect("TODO");
let guild_vcs = vcs.get(&guild_id).expect("TODO");
tracing::error!(?guild_vcs, "TODO");
return;
}
};
let (&channel_id, user_in_vc_data) = guild_vcs.get_left_and_data_for(&user_id).expect("TODO");
tracing::error!(?user_in_vc_data, "TODO");
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::DeferredChannelMessageWithSource,
data: None,
},
)
.await
.expect("TODO");
let call = tokio::spawn({
let songbird = state.songbird.clone();
@@ -44,5 +134,20 @@ pub async fn handle(state: State, interaction: Interaction) {
.unwrap()
.expect("TODO");
let channel_mention = format!("<#{channel_id}>");
state
.discord_client
.interaction(state.discord_application_id)
.update_response(
&interaction.token,
).embeds(Some(&[
EmbedBuilder::new().title("Joined VC with intent to record").description(&format!("This bot joined {channel_mention} and intends to start recording after a responsible disclosure and consent.")).validate().unwrap().build()
]))
.await
.expect("TODO");
tracing::error!(?call, "TODO");
let call_guard = call.lock().await;
}

View File

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

View File

@@ -132,7 +132,7 @@ async fn main() -> Result<(), MainError> {
.await
.expect("couldn't get current Discord application"); // TODO
let application_id = current_application.id;
let discord_application_id = current_application.id;
let shard_id = ShardId::new(0, 1);
let intents = Intents::GUILD_VOICE_STATES;
@@ -150,7 +150,7 @@ async fn main() -> Result<(), MainError> {
let event_types = EventTypeFlags::GUILD_VOICE_STATES | EventTypeFlags::INTERACTION_CREATE;
let mut next_event = shard.next_event(event_types);
let interaction_client = discord_client.interaction(application_id);
let interaction_client = discord_client.interaction(discord_application_id);
let commands = all_commands();
@@ -172,11 +172,17 @@ async fn main() -> Result<(), MainError> {
let command_router = CommandRouter::from_iter(commands);
let vcs = initialize_vcs(&discord_client).await;
let discord_client = Arc::new(discord_client);
let songbird = Arc::new(songbird);
let vcs = Arc::new(vcs);
let songbird = Arc::new(songbird);
let state = State { vcs, songbird };
let state = State {
discord_application_id,
discord_client,
songbird,
vcs,
};
while let Some(event_res) = next_event.await {
match event_res {