feat: implement RecordingDataManager::between and between_in_vc, start using it in /render

This commit is contained in:
2026-05-27 21:48:31 -04:00
parent 23f86ace3b
commit 0137f97788
3 changed files with 201 additions and 41 deletions

View File

@@ -1,3 +1,4 @@
use futures::TryStreamExt as _;
use snafu::{OptionExt as _, Report, ResultExt as _, Snafu}; use snafu::{OptionExt as _, Report, ResultExt as _, Snafu};
use std::{collections::BTreeMap, sync::LazyLock}; use std::{collections::BTreeMap, sync::LazyLock};
use time::{UtcDateTime, format_description::well_known::Rfc3339}; use time::{UtcDateTime, format_description::well_known::Rfc3339};
@@ -85,7 +86,7 @@ pub static COMMAND: LazyLock<Command> = LazyLock::new(|| {
}); });
struct Options { struct Options {
voice_channel: Id<ChannelMarker>, voice_channel_id: Id<ChannelMarker>,
start: UtcDateTime, start: UtcDateTime,
end: UtcDateTime, end: UtcDateTime,
} }
@@ -152,15 +153,15 @@ fn parse_options(interaction: &Interaction) -> Result<Options, ParseOptionsError
}) })
.collect(); .collect();
let voice_channel = options let voice_channel_id = options
.remove(OPTION_VOICE_CHANNEL) .remove(OPTION_VOICE_CHANNEL)
.context(NoVoiceChannelSnafu)?; .context(NoVoiceChannelSnafu)?;
let start = options.remove(OPTION_START).context(NoStartSnafu)?; let start = options.remove(OPTION_START).context(NoStartSnafu)?;
let end = options.remove(OPTION_END).context(NoEndSnafu)?; let end = options.remove(OPTION_END).context(NoEndSnafu)?;
let &CommandOptionValue::Channel(voice_channel) = voice_channel else { let &CommandOptionValue::Channel(voice_channel_id) = voice_channel_id else {
return Err(ParseOptionsError::VoiceChannelInvalidType { return Err(ParseOptionsError::VoiceChannelInvalidType {
actual: voice_channel.clone(), actual: voice_channel_id.clone(),
}); });
}; };
let &CommandOptionValue::String(ref start) = start else { let &CommandOptionValue::String(ref start) = start else {
@@ -178,7 +179,7 @@ fn parse_options(interaction: &Interaction) -> Result<Options, ParseOptionsError
let end = UtcDateTime::parse(end, &DATE_FORMAT).context(EndInvalidDateSnafu)?; let end = UtcDateTime::parse(end, &DATE_FORMAT).context(EndInvalidDateSnafu)?;
Ok(Options { Ok(Options {
voice_channel, voice_channel_id,
start, start,
end, end,
}) })
@@ -186,6 +187,36 @@ fn parse_options(interaction: &Interaction) -> Result<Options, ParseOptionsError
#[tracing::instrument] #[tracing::instrument]
pub async fn handle(state: State, interaction: Interaction) { pub async fn handle(state: State, interaction: Interaction) {
let Some(guild_id) = interaction.guild_id else {
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("Not in a server")
.description(
"This command can only work when used in a Discord server.",
)
.validate()
.unwrap()
.build()])
.flags(MessageFlags::EPHEMERAL)
.build(),
),
},
)
.await
.expect("TODO");
return;
};
let bot_owner_user_id = state.discord_bot_owner_user_id; let bot_owner_user_id = state.discord_bot_owner_user_id;
let is_bot_owner = interaction let is_bot_owner = interaction
@@ -224,7 +255,7 @@ pub async fn handle(state: State, interaction: Interaction) {
} }
let Options { let Options {
voice_channel, voice_channel_id,
start, start,
end, end,
} = match parse_options(&interaction) { } = match parse_options(&interaction) {
@@ -253,9 +284,22 @@ pub async fn handle(state: State, interaction: Interaction) {
} }
}; };
tracing::info!(?voice_channel, ?start, ?end);
let duration = end - start; let duration = end - start;
tracing::info!(?voice_channel_id, ?start, ?end, ?duration);
let mut recordings =
state
.recording_data_manager
.between_in_vc(start, end, guild_id, voice_channel_id);
while let Some(recording) = recordings.try_next().await.expect("TODO") {
tracing::debug!(?recording);
let recording_data = state
.recording_data_manager
.read(&recording)
.await
.expect("TODO");
tracing::debug!(?recording, ?recording_data);
}
todo!(); todo!();
} }

View File

@@ -1,44 +1,159 @@
use futures::{TryStream, TryStreamExt as _}; use futures::{SinkExt, StreamExt as _, TryStream, TryStreamExt as _};
use snafu::{ResultExt as _, Snafu}; use snafu::Snafu;
use std::{fmt::Display, str::FromStr}; use time::{Month, UtcDateTime};
use time::UtcDateTime;
use twilight_model::id::{ use twilight_model::id::{
Id, Id,
marker::{ChannelMarker, GuildMarker}, marker::{ChannelMarker, GuildMarker},
}; };
use super::{ListError, Recording, RecordingDataManager, recording}; use super::{ClipEntryError, ListError, Recording, RecordingDataManager};
const BUFFER_SIZE: usize = 2048;
#[derive(Debug, Snafu)] #[derive(Debug, Snafu)]
pub enum RecordingEntryError { pub enum RecordingEntryError {
/// failed to get an entry from the storage operator's lister /// could not list (at least some) clips
ReceiveEntryError { source: opendal::Error }, ListClipsError { source: ListError },
/// failed to parse the entry as a recording /// could not receive this clip entry
ParseError { source: recording::TakeError }, ClipEntryError { source: ClipEntryError },
} }
// impl RecordingDataManager { impl RecordingDataManager {
// pub async fn between( pub fn between(
// &self, &self,
// start: UtcDateTime, start: UtcDateTime,
// end: UtcDateTime, end: UtcDateTime,
// ) -> Result<impl TryStream<Ok = Recording, Error = RecordingEntryError> + Unpin, ListError> ) -> impl TryStream<Ok = Recording, Error = RecordingEntryError> + Unpin {
// { let this = self.clone();
// todo!();
// }
// }
// impl RecordingDataManager { let (mut sink, stream) = futures::channel::mpsc::channel(BUFFER_SIZE);
// pub async fn between_in_vc(
// &self, tokio::spawn(async move {
// start: UtcDateTime, let year_start = start.year();
// end: UtcDateTime, let year_end = end.year();
// guild_id: Id<GuildMarker>,
// voice_channel_id: Id<ChannelMarker>, let years = year_start..=year_end;
// ) -> Result<impl TryStream<Ok = Recording, Error = RecordingEntryError> + Unpin, ListError>
// { for year in years {
// todo!(); let mut month_start = start.month();
// Ok(self.between(start, end)?) let mut month_end = end.month();
// }
// } if year > year_start {
month_start = Month::January;
}
if year < year_end {
month_end = Month::December;
}
let months = month_start as u8..=month_end as u8;
let months = months.map(|month| Month::try_from(month).unwrap());
for month in months {
let mut day_start = start.day();
let mut day_end = end.day();
if month > month_start {
day_start = 1;
}
if month < month_end {
day_end = 31;
}
let days = day_start..=day_end;
for day in days {
let mut hour_start = start.hour();
let mut hour_end = end.hour();
if day > day_start {
hour_start = 0;
}
if day < day_end {
hour_end = 23;
}
let hours = hour_start..=hour_end;
for hour in hours {
let mut minute_start = start.minute();
let mut minute_end = end.minute();
if hour > hour_start {
minute_start = 0;
}
if hour < hour_end {
minute_end = 59;
}
let minutes = minute_start..=minute_end;
for minute in minutes {
match this.clips(year, month, day, hour, minute).await {
Err(list_error) => {
let _ = sink
.send(Err(RecordingEntryError::ListClipsError {
source: list_error,
}))
.await;
}
Ok(clips) => {
let mut clips = clips.into_stream();
while let Some(clip_result) = clips.next().await {
match clip_result {
Err(entry_error) => {
let _ = sink
.send(Err(
RecordingEntryError::ClipEntryError {
source: entry_error,
},
))
.await;
}
Ok(clip) => {
let recording = Recording {
year,
month,
day,
hour,
minute,
clip,
};
let _ = sink.send(Ok(recording)).await;
}
}
}
}
}
}
}
}
}
}
});
stream
}
}
impl RecordingDataManager {
pub fn between_in_vc(
&self,
start: UtcDateTime,
end: UtcDateTime,
guild_id: Id<GuildMarker>,
voice_channel_id: Id<ChannelMarker>,
) -> impl TryStream<Ok = Recording, Error = RecordingEntryError> + Unpin {
self.between(start, end).try_filter(move |recording| {
std::future::ready(
recording.clip.guild == guild_id
&& recording.clip.voice_channel == voice_channel_id,
)
})
}
}

View File

@@ -18,7 +18,8 @@ mod user;
mod voice_channel; mod voice_channel;
mod year; mod year;
pub use clip::Clip; pub use between::RecordingEntryError;
pub use clip::{Clip, ClipEntryError};
use day::Day; use day::Day;
use guild::Guild; use guild::Guild;
use hour::Hour; use hour::Hour;