feat: inspect user data

This commit is contained in:
2026-04-21 15:06:56 -04:00
parent 0ce26fc0e5
commit 7fe6980867
2 changed files with 123 additions and 8 deletions

View File

@@ -2,7 +2,7 @@ use std::sync::LazyLock;
use async_compression::futures::bufread::BrotliDecoder; use async_compression::futures::bufread::BrotliDecoder;
use capnp::message::ReaderOptions; use capnp::message::ReaderOptions;
use futures::AsyncReadExt; use futures::{AsyncReadExt, TryStreamExt};
use opendal::ErrorKind; use opendal::ErrorKind;
use snafu::{OptionExt, Snafu}; use snafu::{OptionExt, Snafu};
use twilight_model::{ use twilight_model::{
@@ -175,4 +175,48 @@ pub async fn handle(state: State, interaction: Interaction) {
.flags(MessageFlags::EPHEMERAL) .flags(MessageFlags::EPHEMERAL)
.await .await
.expect("TODO"); .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");
}
} }

View File

@@ -1,11 +1,13 @@
use std::str::FromStr;
use async_compression::futures::{bufread::BrotliDecoder, write::BrotliEncoder}; use async_compression::futures::{bufread::BrotliDecoder, write::BrotliEncoder};
use capnp::message::{TypedBuilder, TypedReader}; use capnp::message::{TypedBuilder, TypedReader};
use futures::{AsyncReadExt, AsyncWriteExt}; use futures::{AsyncReadExt, AsyncWriteExt, TryStream, TryStreamExt};
use opendal::Operator; use opendal::Operator;
use snafu::{ResultExt, Snafu}; use snafu::{OptionExt as _, ResultExt as _, Snafu, ensure};
use twilight_model::id::{Id, marker::UserMarker}; 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)] #[derive(Debug, Clone)]
pub struct UserDataManager { pub struct UserDataManager {
@@ -16,9 +18,78 @@ impl UserDataManager {
pub fn new(operator: Operator) -> Self { pub fn new(operator: Operator) -> Self {
Self { operator } Self { operator }
} }
}
fn path(id: Id<UserMarker>) -> String { fn path(id: Id<UserMarker>) -> String {
format!("{id}/data.bin.brotli") 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: <Id<UserMarker> as FromStr>::Err,
},
}
fn parse(path: &str) -> Result<Id<UserMarker>, 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<impl TryStream<Ok = Id<UserMarker>, 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<R, WithError> { ) -> Result<R, WithError> {
let compressed_buffer = self let compressed_buffer = self
.operator .operator
.async_reader_if_exists(&UserDataManager::path(id)) .async_reader_if_exists(&path(id))
.await .await
.context(with_error::ReadSnafu)?; .context(with_error::ReadSnafu)?;
@@ -108,7 +179,7 @@ impl UserDataManager {
id: Id<UserMarker>, id: Id<UserMarker>,
f: impl FnOnce(user_capnp::user::Builder<'_>) -> R, f: impl FnOnce(user_capnp::user::Builder<'_>) -> R,
) -> Result<R, UpdateError> { ) -> Result<R, UpdateError> {
let path = UserDataManager::path(id); let path = path(id);
let compressed_buffer = self let compressed_buffer = self
.operator .operator
.async_reader_if_exists(&path) .async_reader_if_exists(&path)