feat: more work on /render

This commit is contained in:
2026-05-26 00:10:52 -04:00
parent 453208ff17
commit f86c094dda
2 changed files with 224 additions and 6 deletions

View File

@@ -69,7 +69,7 @@ songbird = { version = "0.6.0", default-features = false, features = [
"tws", "tws",
] } ] }
strum = { version = "0.28.0", features = ["derive"] } strum = { version = "0.28.0", features = ["derive"] }
time = "0.3.47" time = { version = "0.3.47", features = ["formatting", "parsing"] }
tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros", "signal", "time"] } tokio = { version = "1.46.0", features = ["rt-multi-thread", "macros", "signal", "time"] }
tokio-util = { version = "0.7.18", features = ["io"] } tokio-util = { version = "0.7.18", features = ["io"] }
tokio-websockets-0-13 = { package = "tokio-websockets", version = "0.13", features = [ tokio-websockets-0-13 = { package = "tokio-websockets", version = "0.13", features = [

View File

@@ -1,9 +1,18 @@
use std::sync::LazyLock; use snafu::{OptionExt as _, Report, ResultExt as _, Snafu};
use twilight_model::application::{ use std::{collections::BTreeMap, sync::LazyLock};
command::{Command, CommandType}, use time::{UtcDateTime, format_description::well_known::Rfc3339};
interaction::Interaction, use twilight_model::{
application::{
command::{Command, CommandOption, CommandOptionType, CommandType},
interaction::{Interaction, InteractionData, application_command::CommandOptionValue},
},
channel::{ChannelType, message::{Embed, MessageFlags}},
http::interaction::{InteractionResponse, InteractionResponseType},
id::{Id, marker::ChannelMarker},
};
use twilight_util::builder::{
InteractionResponseDataBuilder, command::CommandBuilder, embed::EmbedBuilder,
}; };
use twilight_util::builder::command::CommandBuilder;
use crate::command::State; use crate::command::State;
@@ -11,13 +20,162 @@ const NAME: &str = "render";
const DESCRIPTION: &str = const DESCRIPTION: &str =
"(Only the bot owner can use this) Make an audio file from the specified range of VC"; "(Only the bot owner can use this) Make an audio file from the specified range of VC";
const OPTION_VOICE_CHANNEL: &str = "voice-channel";
const OPTION_START: &str = "start";
const OPTION_END: &str = "end";
const DATE_FORMAT: Rfc3339 = Rfc3339;
pub static COMMAND: LazyLock<Command> = LazyLock::new(|| { pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput) CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput)
.option(CommandOption {
autocomplete: None,
channel_types: Some(vec![ChannelType::GuildVoice]),
choices: None,
description: "Which voice channel to render a recording from".into(),
description_localizations: None,
kind: CommandOptionType::Channel,
max_length: None,
max_value: None,
min_length: None,
min_value: None,
name: OPTION_VOICE_CHANNEL.into(),
name_localizations: None,
options: None,
required: Some(true),
})
.option(CommandOption {
autocomplete: None,
channel_types: None,
choices: None,
description: "What UTC datetime to start from".into(),
description_localizations: None,
kind: CommandOptionType::String,
max_length: None,
max_value: None,
min_length: None,
min_value: None,
name: OPTION_START.into(),
name_localizations: None,
options: None,
required: Some(true),
})
.option(CommandOption {
autocomplete: None,
channel_types: None,
choices: None,
description: "What UTC datetime to end at".into(),
description_localizations: None,
kind: CommandOptionType::String,
max_length: None,
max_value: None,
min_length: None,
min_value: None,
name: OPTION_END.into(),
name_localizations: None,
options: None,
required: Some(true),
})
.validate() .validate()
.expect("command wasn't correct") .expect("command wasn't correct")
.build() .build()
}); });
struct Options {
voice_channel: Id<ChannelMarker>,
start: UtcDateTime,
end: UtcDateTime,
}
#[derive(Debug, Snafu)]
enum ParseOptionsError {
/// could not get any interaction data
NoInteractionData,
/// this wasn't a command invocation
NotCommandInvocation,
/// a voice channel wasn't selected
NoVoiceChannel,
/// a start time wasn't specified
NoStart,
/// an end time wasn't specified
NoEnd,
/// voice channel was {actual:?} instead of a channel ID
VoiceChannelInvalidType { actual: CommandOptionValue },
/// start was {actual:?} instead of a string
StartInvalidType { actual: CommandOptionValue },
/// end was {actual:?} instead of a string
EndInvalidType { actual: CommandOptionValue },
/// could not parse `start` as a date in RFC3339 format
StartInvalidDate { source: time::error::Parse },
/// could not parse `start` as a date in RFC3339 format
EndInvalidDate { source: time::error::Parse },
}
impl From<ParseOptionsError> for Embed {
fn from(error: ParseOptionsError) -> Self {
EmbedBuilder::new().title("Error parsing options").description(Report::from_error(error).to_string()).validate().unwrap().build()
}
}
fn parse_options(interaction: &Interaction) -> Result<Options, ParseOptionsError> {
let interaction_data = interaction.data.as_ref().context(NoInteractionDataSnafu)?;
let InteractionData::ApplicationCommand(command_data) = interaction_data else {
return Err(ParseOptionsError::NotCommandInvocation);
};
let mut options: BTreeMap<&str, &CommandOptionValue> = command_data
.options
.iter()
.map(|command_data_option| {
(
command_data_option.name.as_str(),
&command_data_option.value,
)
})
.collect();
let voice_channel = options
.remove(OPTION_VOICE_CHANNEL)
.context(NoVoiceChannelSnafu)?;
let start = options.remove(OPTION_START).context(NoStartSnafu)?;
let end = options.remove(OPTION_END).context(NoEndSnafu)?;
let &CommandOptionValue::Channel(voice_channel) = voice_channel else {
return Err(ParseOptionsError::VoiceChannelInvalidType {
actual: voice_channel.clone(),
});
};
let &CommandOptionValue::String(ref start) = start else {
return Err(ParseOptionsError::StartInvalidType {
actual: start.clone(),
});
};
let &CommandOptionValue::String(ref end) = end else {
return Err(ParseOptionsError::StartInvalidType {
actual: end.clone(),
});
};
let start = UtcDateTime::parse(start, &DATE_FORMAT).context(StartInvalidDateSnafu)?;
let end = UtcDateTime::parse(end, &DATE_FORMAT).context(EndInvalidDateSnafu)?;
Ok(Options {
voice_channel,
start,
end,
})
}
#[tracing::instrument] #[tracing::instrument]
pub async fn handle(state: State, interaction: Interaction) { pub async fn handle(state: State, interaction: Interaction) {
let bot_owner_user_id = state.discord_bot_owner_user_id; let bot_owner_user_id = state.discord_bot_owner_user_id;
@@ -29,5 +187,65 @@ pub async fn handle(state: State, interaction: Interaction) {
.map(|user_id| user_id == bot_owner_user_id) .map(|user_id| user_id == bot_owner_user_id)
.unwrap_or(false); .unwrap_or(false);
if !is_bot_owner {
state
.discord_client
.interaction(state.discord_application_id)
.create_response(
interaction.id,
&interaction.token,
&InteractionResponse {
kind: InteractionResponseType::ChannelMessageWithSource,
data: Some(
InteractionResponseDataBuilder::new()
.embeds([EmbedBuilder::new()
.title("No permission")
.description("Only the bot owner can use this command.")
.validate()
.unwrap()
.build()])
.flags(MessageFlags::EPHEMERAL)
.build(),
),
},
)
.await
.expect("TODO");
return;
}
let Options {
voice_channel,
start,
end,
} = match parse_options(&interaction) {
Ok(options) => options,
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");
return;
}
};
tracing::info!(?voice_channel, ?start, ?end);
todo!(); todo!();
} }