Compare commits
2 Commits
cfe6ddf218
...
f86c094dda
| Author | SHA1 | Date | |
|---|---|---|---|
| f86c094dda | |||
| 453208ff17 |
@@ -69,7 +69,7 @@ songbird = { version = "0.6.0", default-features = false, features = [
|
||||
"tws",
|
||||
] }
|
||||
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-util = { version = "0.7.18", features = ["io"] }
|
||||
tokio-websockets-0-13 = { package = "tokio-websockets", version = "0.13", features = [
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
use std::sync::LazyLock;
|
||||
use twilight_model::application::{
|
||||
command::{Command, CommandType},
|
||||
interaction::Interaction,
|
||||
use snafu::{OptionExt as _, Report, ResultExt as _, Snafu};
|
||||
use std::{collections::BTreeMap, sync::LazyLock};
|
||||
use time::{UtcDateTime, format_description::well_known::Rfc3339};
|
||||
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;
|
||||
|
||||
@@ -11,13 +20,162 @@ const NAME: &str = "render";
|
||||
const DESCRIPTION: &str =
|
||||
"(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(|| {
|
||||
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()
|
||||
.expect("command wasn't correct")
|
||||
.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]
|
||||
pub async fn handle(state: State, interaction: Interaction) {
|
||||
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)
|
||||
.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!();
|
||||
}
|
||||
|
||||
@@ -3,23 +3,30 @@ use opendal::{Buffer, Error, ErrorKind, FuturesAsyncReader, Operator};
|
||||
|
||||
#[extension(pub trait OperatorExt)]
|
||||
impl Operator {
|
||||
async fn read_if_exists(&self, path: &str) -> Result<Option<Buffer>, Error> {
|
||||
match self.read(path).await {
|
||||
Ok(buffer) => Ok(Some(buffer)),
|
||||
Err(error) if matches!(error.kind(), ErrorKind::NotFound) => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
fn read_if_exists(
|
||||
&self,
|
||||
path: &str,
|
||||
) -> impl Future<Output = Result<Option<Buffer>, Error>> + Send {
|
||||
async {
|
||||
match self.read(path).await {
|
||||
Ok(buffer) => Ok(Some(buffer)),
|
||||
Err(error) if matches!(error.kind(), ErrorKind::NotFound) => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn async_reader_if_exists(
|
||||
fn async_reader_if_exists(
|
||||
&self,
|
||||
path: &str,
|
||||
) -> Result<Option<FuturesAsyncReader>, Error> {
|
||||
let reader = self.reader(path).await?;
|
||||
match reader.into_futures_async_read(..).await {
|
||||
Ok(reader) => Ok(Some(reader)),
|
||||
Err(error) if matches!(error.kind(), ErrorKind::NotFound) => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
) -> impl Future<Output = Result<Option<FuturesAsyncReader>, Error>> + Send {
|
||||
async {
|
||||
let reader = self.reader(path).await?;
|
||||
match reader.into_futures_async_read(..).await {
|
||||
Ok(reader) => Ok(Some(reader)),
|
||||
Err(error) if matches!(error.kind(), ErrorKind::NotFound) => Ok(None),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user