From 7fe698086700eba01cbb49d6b21db667e78d29b2 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 21 Apr 2026 15:06:56 -0400 Subject: [PATCH] feat: inspect user data --- src/command/debug.rs | 46 +++++++++++++++++++++++- src/user_data.rs | 85 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 123 insertions(+), 8 deletions(-) diff --git a/src/command/debug.rs b/src/command/debug.rs index d6aea4a..b5c6fa5 100644 --- a/src/command/debug.rs +++ b/src/command/debug.rs @@ -2,7 +2,7 @@ use std::sync::LazyLock; use async_compression::futures::bufread::BrotliDecoder; use capnp::message::ReaderOptions; -use futures::AsyncReadExt; +use futures::{AsyncReadExt, TryStreamExt}; use opendal::ErrorKind; use snafu::{OptionExt, Snafu}; use twilight_model::{ @@ -175,4 +175,48 @@ pub async fn handle(state: State, interaction: Interaction) { .flags(MessageFlags::EPHEMERAL) .await .expect("TODO"); + + let mut user_id_stream = state.user_data_manager.list().await.expect("TODO"); + + while let Some(user_id) = user_id_stream.try_next().await.expect("TODO") { + let (consent, notification_script) = state + .user_data_manager + .with(user_id, |user_data| { + let consent = user_data.get_voice_recording_consent().unwrap(); + let notification_script = user_data.has_notification_script().then_some( + user_data + .get_notification_script() + .expect("TODO") + .to_string() + .expect("TODO"), + ); + + (consent, notification_script) + }) + .await + .expect("TODO"); + + let user_mention = format!("<@{user_id}>"); + + state + .discord_client + .interaction(state.discord_application_id) + .create_followup(&interaction.token) + .embeds(&[EmbedBuilder::new() + .title(user_mention) + .field(EmbedFieldBuilder::new("Consent", format!("{consent:?}")).build()) + .field( + EmbedFieldBuilder::new( + "Notification Script", + format!("{notification_script:?}"), + ) + .build(), + ) + .validate() + .unwrap() + .build()]) + .flags(MessageFlags::EPHEMERAL) + .await + .expect("TODO"); + } } diff --git a/src/user_data.rs b/src/user_data.rs index fd706ac..6f7c19c 100644 --- a/src/user_data.rs +++ b/src/user_data.rs @@ -1,11 +1,13 @@ +use std::str::FromStr; + use async_compression::futures::{bufread::BrotliDecoder, write::BrotliEncoder}; use capnp::message::{TypedBuilder, TypedReader}; -use futures::{AsyncReadExt, AsyncWriteExt}; +use futures::{AsyncReadExt, AsyncWriteExt, TryStream, TryStreamExt}; use opendal::Operator; -use snafu::{ResultExt, Snafu}; +use snafu::{OptionExt as _, ResultExt as _, Snafu, ensure}; use twilight_model::id::{Id, marker::UserMarker}; -use crate::{OperatorExt, option_ext::OptionExt, user_capnp}; +use crate::{OperatorExt, option_ext::OptionExt as _, user_capnp}; #[derive(Debug, Clone)] pub struct UserDataManager { @@ -16,9 +18,78 @@ impl UserDataManager { pub fn new(operator: Operator) -> Self { Self { operator } } +} - fn path(id: Id) -> String { - format!("{id}/data.bin.brotli") +fn path(id: Id) -> String { + format!("{id}/data.bin.brotli") +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum ParsePathError { + /// paths must have a / in them because that's how user data is stored, but this one doesn't have one + MissingSlashError, + + /// if this isn't a directory, then the file must be "data.bin.brotli" but it was actually {actual:?} + WrongFileError { actual: String }, + + /// couldn't parse the directory as a user ID + ParseUserIdError { + source: as FromStr>::Err, + }, +} + +fn parse(path: &str) -> Result, ParsePathError> { + let (directory, file) = path + .rsplit_once("/") + .context(parse_path_error::MissingSlashSnafu)?; + + ensure!( + file == "" || file == "data.bin.brotli", + parse_path_error::WrongFileSnafu { + actual: file.to_owned() + } + ); + + let user_id = directory + .parse() + .context(parse_path_error::ParseUserIdSnafu)?; + + Ok(user_id) +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum ListError { + /// error creating a lister through the storage operator + CreateListerError { source: opendal::Error }, +} + +#[derive(Debug, Snafu)] +#[snafu(module)] +pub enum EntryError { + /// failed to get an entry from the storage operator's lister + ReceiveEntryError { source: opendal::Error }, + + /// failed to parse the entry as an acceptable path + ParsePathError { source: ParsePathError }, +} + +impl UserDataManager { + pub async fn list( + &self, + ) -> Result, Error = EntryError> + Unpin, ListError> { + let lister = self + .operator + .lister("") + .await + .context(list_error::CreateListerSnafu)?; + + Ok(lister + .map_err(|error| EntryError::ReceiveEntryError { source: error }) + .and_then(|entry| { + std::future::ready(parse(entry.path()).context(entry_error::ParsePathSnafu)) + })) } } @@ -43,7 +114,7 @@ impl UserDataManager { ) -> Result { let compressed_buffer = self .operator - .async_reader_if_exists(&UserDataManager::path(id)) + .async_reader_if_exists(&path(id)) .await .context(with_error::ReadSnafu)?; @@ -108,7 +179,7 @@ impl UserDataManager { id: Id, f: impl FnOnce(user_capnp::user::Builder<'_>) -> R, ) -> Result { - let path = UserDataManager::path(id); + let path = path(id); let compressed_buffer = self .operator .async_reader_if_exists(&path)