feat: more work on /render
This commit is contained in:
@@ -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 = [
|
||||||
|
|||||||
@@ -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!();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user