From a77b7717b892b78d35ac957f12110fad6f895a0c Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 21 May 2023 15:36:36 +0100 Subject: [PATCH] feat: generate and fetch multiple snapshots --- .../delta/src/routes/safety/fetch_snapshot.rs | 82 ------------- .../src/routes/safety/fetch_snapshots.rs | 87 ++++++++++++++ crates/delta/src/routes/safety/mod.rs | 4 +- .../delta/src/routes/safety/report_content.rs | 110 ++++++------------ .../quark/src/impl/dummy/safety/snapshot.rs | 2 +- crates/quark/src/impl/generic/mod.rs | 1 + .../quark/src/impl/generic/safety/snapshot.rs | 91 +++++++++++++++ .../quark/src/impl/mongo/safety/snapshot.rs | 4 +- crates/quark/src/traits/safety/snapshot.rs | 4 +- 9 files changed, 220 insertions(+), 165 deletions(-) delete mode 100644 crates/delta/src/routes/safety/fetch_snapshot.rs create mode 100644 crates/delta/src/routes/safety/fetch_snapshots.rs create mode 100644 crates/quark/src/impl/generic/safety/snapshot.rs diff --git a/crates/delta/src/routes/safety/fetch_snapshot.rs b/crates/delta/src/routes/safety/fetch_snapshot.rs deleted file mode 100644 index 7d32bcf2..00000000 --- a/crates/delta/src/routes/safety/fetch_snapshot.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::collections::HashSet; - -use revolt_quark::models::snapshot::{SnapshotContent, SnapshotWithContext}; -use revolt_quark::models::{Channel, User}; -use revolt_quark::{Db, Error, Result}; -use rocket::serde::json::Json; - -/// # Fetch Snapshot -/// -/// Fetch a snapshot for a given report -#[openapi(tag = "User Safety")] -#[get("/snapshot/")] -pub async fn fetch_snapshot( - db: &Db, - user: User, - report_id: String, -) -> Result> { - // Must be privileged for this route - if !user.privileged { - return Err(Error::NotPrivileged); - } - - // Fetch snapshot - let snapshot = db.fetch_snapshot(&report_id).await?; - - // Resolve and fetch IDs of associated content - let mut user_ids: HashSet<&str> = HashSet::new(); - let mut channel_ids: HashSet<&str> = HashSet::new(); - - match &snapshot.content { - SnapshotContent::Message { - prior_context, - leading_context, - message, - } => { - for msg in prior_context { - user_ids.insert(&msg.author); - } - - for msg in leading_context { - user_ids.insert(&msg.author); - } - - user_ids.insert(&message.author); - channel_ids.insert(&message.channel); - } - SnapshotContent::User(user) => { - user_ids.insert(&user.id); - } - SnapshotContent::Server(server) => { - for channel in &server.channels { - channel_ids.insert(channel); - } - } - } - - // Collect user and channel IDs - let user_ids: Vec = user_ids.into_iter().map(|s| s.to_owned()).collect(); - let channel_ids: Vec = channel_ids.into_iter().map(|s| s.to_owned()).collect(); - - // Fetch users and channels - let users = db.fetch_users(&user_ids).await?; - let channels = db.fetch_channels(&channel_ids).await?; - - // Pull out first server from channels if possible - let server = if let Some(server_id) = channels.iter().find_map(|channel| match channel { - Channel::TextChannel { server, .. } => Some(server), - _ => None, - }) { - Some(db.fetch_server(server_id).await?) - } else { - None - }; - - // Return snapshot with context - Ok(Json(SnapshotWithContext { - snapshot, - users, - channels, - server, - })) -} diff --git a/crates/delta/src/routes/safety/fetch_snapshots.rs b/crates/delta/src/routes/safety/fetch_snapshots.rs new file mode 100644 index 00000000..8283b48f --- /dev/null +++ b/crates/delta/src/routes/safety/fetch_snapshots.rs @@ -0,0 +1,87 @@ +use std::collections::HashSet; + +use revolt_quark::models::snapshot::{SnapshotContent, SnapshotWithContext}; +use revolt_quark::models::{Channel, User}; +use revolt_quark::{Db, Error, Result}; +use rocket::serde::json::Json; + +/// # Fetch Snapshots +/// +/// Fetch a snapshots for a given report +#[openapi(tag = "User Safety")] +#[get("/snapshot/")] +pub async fn fetch_snapshots( + db: &Db, + user: User, + report_id: String, +) -> Result>> { + // Must be privileged for this route + if !user.privileged { + return Err(Error::NotPrivileged); + } + + // Fetch snapshots + let snapshots = db.fetch_snapshots(&report_id).await?; + let mut result = vec![]; + + for snapshot in snapshots { + // Resolve and fetch IDs of associated content + let mut user_ids: HashSet<&str> = HashSet::new(); + let mut channel_ids: HashSet<&str> = HashSet::new(); + + match &snapshot.content { + SnapshotContent::Message { + prior_context, + leading_context, + message, + } => { + for msg in prior_context { + user_ids.insert(&msg.author); + } + + for msg in leading_context { + user_ids.insert(&msg.author); + } + + user_ids.insert(&message.author); + channel_ids.insert(&message.channel); + } + SnapshotContent::User(user) => { + user_ids.insert(&user.id); + } + SnapshotContent::Server(server) => { + for channel in &server.channels { + channel_ids.insert(channel); + } + } + } + + // Collect user and channel IDs + let user_ids: Vec = user_ids.into_iter().map(|s| s.to_owned()).collect(); + let channel_ids: Vec = channel_ids.into_iter().map(|s| s.to_owned()).collect(); + + // Fetch users and channels + let users = db.fetch_users(&user_ids).await?; + let channels = db.fetch_channels(&channel_ids).await?; + + // Pull out first server from channels if possible + let server = if let Some(server_id) = channels.iter().find_map(|channel| match channel { + Channel::TextChannel { server, .. } => Some(server), + _ => None, + }) { + Some(db.fetch_server(server_id).await?) + } else { + None + }; + + // Return snapshot with context + result.push(SnapshotWithContext { + snapshot, + users, + channels, + server, + }); + } + + Ok(Json(result)) +} diff --git a/crates/delta/src/routes/safety/mod.rs b/crates/delta/src/routes/safety/mod.rs index 7aee1e8a..7487fef4 100644 --- a/crates/delta/src/routes/safety/mod.rs +++ b/crates/delta/src/routes/safety/mod.rs @@ -6,7 +6,7 @@ mod fetch_report; mod fetch_reports; mod report_content; -mod fetch_snapshot; +mod fetch_snapshots; pub fn routes() -> (Vec, OpenApi) { openapi_get_routes_spec![ @@ -16,6 +16,6 @@ pub fn routes() -> (Vec, OpenApi) { fetch_reports::fetch_reports, report_content::report_content, // Snapshots - fetch_snapshot::fetch_snapshot + fetch_snapshots::fetch_snapshots ] } diff --git a/crates/delta/src/routes/safety/report_content.rs b/crates/delta/src/routes/safety/report_content.rs index 5a7060b7..ffce8650 100644 --- a/crates/delta/src/routes/safety/report_content.rs +++ b/crates/delta/src/routes/safety/report_content.rs @@ -1,5 +1,4 @@ use revolt_quark::events::client::EventV1; -use revolt_quark::models::message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod}; use revolt_quark::models::report::{ReportStatus, ReportedContent}; use revolt_quark::models::snapshot::{Snapshot, SnapshotContent}; use revolt_quark::models::{Report, User}; @@ -38,7 +37,7 @@ pub async fn report_content(db: &Db, user: User, data: Json) // Find the content and create a snapshot of it // Also retrieve any references to Files - let (content, files): (SnapshotContent, Vec) = match &data.content { + let (snapshots, files): (Vec, Vec) = match &data.content { ReportedContent::Message { id, .. } => { let message = db.fetch_message(id).await?; @@ -47,53 +46,8 @@ pub async fn report_content(db: &Db, user: User, data: Json) return Err(Error::CannotReportYourself); } - // Collect message attachments - let files = message - .attachments - .as_ref() - .map(|attachments| attachments.iter().map(|x| x.id.to_string()).collect()) - .unwrap_or_default(); - - // Collect prior context - let prior_context = db - .fetch_messages(MessageQuery { - filter: MessageFilter { - channel: Some(message.channel.to_string()), - ..Default::default() - }, - limit: Some(15), - time_period: MessageTimePeriod::Absolute { - before: Some(message.id.to_string()), - after: None, - sort: Some(MessageSort::Latest), - }, - }) - .await?; - - // Collect leading context - let leading_context = db - .fetch_messages(MessageQuery { - filter: MessageFilter { - channel: Some(message.channel.to_string()), - ..Default::default() - }, - limit: Some(15), - time_period: MessageTimePeriod::Absolute { - before: None, - after: Some(message.id.to_string()), - sort: Some(MessageSort::Oldest), - }, - }) - .await?; - - ( - SnapshotContent::Message { - message, - prior_context, - leading_context, - }, - files, - ) + let (snapshot, files) = SnapshotContent::generate_from_message(db, message).await?; + (vec![snapshot], files) } ReportedContent::Server { id, .. } => { let server = db.fetch_server(id).await?; @@ -103,15 +57,10 @@ pub async fn report_content(db: &Db, user: User, data: Json) return Err(Error::CannotReportYourself); } - // Collect server's icon and banner - let files = [&server.icon, &server.banner] - .iter() - .filter_map(|x| x.as_ref().map(|x| x.id.to_string())) - .collect(); - - (SnapshotContent::Server(server), files) + let (snapshot, files) = SnapshotContent::generate_from_server(db, server)?; + (vec![snapshot], files) } - ReportedContent::User { id, .. } => { + ReportedContent::User { id, message_id, .. } => { let reported_user = db.fetch_user(id).await?; // Users cannot report themselves @@ -119,19 +68,25 @@ pub async fn report_content(db: &Db, user: User, data: Json) return Err(Error::CannotReportYourself); } - // Collect user's avatar and profile background - let files = [ - reported_user.avatar.as_ref(), - reported_user - .profile - .as_ref() - .and_then(|profile| profile.background.as_ref()), - ] - .iter() - .filter_map(|x| x.as_ref().map(|x| x.id.to_string())) - .collect(); + // Determine if there is a message provided as context + let message = if let Some(id) = message_id { + db.fetch_message(id).await.ok() + } else { + None + }; - (SnapshotContent::User(reported_user), files) + let (snapshot, files) = SnapshotContent::generate_from_user(db, reported_user)?; + + if let Some(message) = message { + let (message_snapshot, message_files) = + SnapshotContent::generate_from_message(db, message).await?; + ( + vec![snapshot, message_snapshot], + [files, message_files].concat(), + ) + } else { + (vec![snapshot], files) + } } }; @@ -143,14 +98,17 @@ pub async fn report_content(db: &Db, user: User, data: Json) // Generate an id for the report let id = Ulid::new().to_string(); - // Save a snapshot of the content - let snapshot = Snapshot { - id: Ulid::new().to_string(), - report_id: id.to_string(), - content, - }; + // Insert all new generated snapshots + for content in snapshots { + // Save a snapshot of the content + let snapshot = Snapshot { + id: Ulid::new().to_string(), + report_id: id.to_string(), + content, + }; - db.insert_snapshot(&snapshot).await?; + db.insert_snapshot(&snapshot).await?; + } // Save the report let report = Report { diff --git a/crates/quark/src/impl/dummy/safety/snapshot.rs b/crates/quark/src/impl/dummy/safety/snapshot.rs index a99447dd..691f28d4 100644 --- a/crates/quark/src/impl/dummy/safety/snapshot.rs +++ b/crates/quark/src/impl/dummy/safety/snapshot.rs @@ -10,7 +10,7 @@ impl AbstractSnapshot for DummyDb { Ok(()) } - async fn fetch_snapshot(&self, _report_id: &str) -> Result { + async fn fetch_snapshots(&self, _report_id: &str) -> Result> { todo!() } } diff --git a/crates/quark/src/impl/generic/mod.rs b/crates/quark/src/impl/generic/mod.rs index 015bc9dd..b050cae5 100644 --- a/crates/quark/src/impl/generic/mod.rs +++ b/crates/quark/src/impl/generic/mod.rs @@ -26,4 +26,5 @@ pub mod users { pub mod safety { pub mod report; + pub mod snapshot; } diff --git a/crates/quark/src/impl/generic/safety/snapshot.rs b/crates/quark/src/impl/generic/safety/snapshot.rs new file mode 100644 index 00000000..73ce36c7 --- /dev/null +++ b/crates/quark/src/impl/generic/safety/snapshot.rs @@ -0,0 +1,91 @@ +use crate::{ + models::{ + message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod}, + snapshot::SnapshotContent, + Message, Server, User, + }, + Database, Result, +}; + +impl SnapshotContent { + pub async fn generate_from_message( + db: &Database, + message: Message, + ) -> Result<(SnapshotContent, Vec)> { + // Collect message attachments + let files = message + .attachments + .as_ref() + .map(|attachments| attachments.iter().map(|x| x.id.to_string()).collect()) + .unwrap_or_default(); + + // Collect prior context + let prior_context = db + .fetch_messages(MessageQuery { + filter: MessageFilter { + channel: Some(message.channel.to_string()), + ..Default::default() + }, + limit: Some(15), + time_period: MessageTimePeriod::Absolute { + before: Some(message.id.to_string()), + after: None, + sort: Some(MessageSort::Latest), + }, + }) + .await?; + + // Collect leading context + let leading_context = db + .fetch_messages(MessageQuery { + filter: MessageFilter { + channel: Some(message.channel.to_string()), + ..Default::default() + }, + limit: Some(15), + time_period: MessageTimePeriod::Absolute { + before: None, + after: Some(message.id.to_string()), + sort: Some(MessageSort::Oldest), + }, + }) + .await?; + + Ok(( + SnapshotContent::Message { + message, + prior_context, + leading_context, + }, + files, + )) + } + + pub fn generate_from_server( + db: &Database, + server: Server, + ) -> Result<(SnapshotContent, Vec)> { + // Collect server's icon and banner + let files = [&server.icon, &server.banner] + .iter() + .filter_map(|x| x.as_ref().map(|x| x.id.to_string())) + .collect(); + + Ok((SnapshotContent::Server(server), files)) + } + + pub fn generate_from_user(db: &Database, user: User) -> Result<(SnapshotContent, Vec)> { + // Collect user's avatar and profile background + let files = [ + user.avatar.as_ref(), + user.profile + .as_ref() + .and_then(|profile| profile.background.as_ref()), + ] + .iter() + .filter_map(|x| x.as_ref().map(|x| x.id.to_string())) + .collect(); + + Ok((SnapshotContent::User(user), files)) + } +} diff --git a/crates/quark/src/impl/mongo/safety/snapshot.rs b/crates/quark/src/impl/mongo/safety/snapshot.rs index 6b31de9f..4b1a934c 100644 --- a/crates/quark/src/impl/mongo/safety/snapshot.rs +++ b/crates/quark/src/impl/mongo/safety/snapshot.rs @@ -11,8 +11,8 @@ impl AbstractSnapshot for MongoDb { self.insert_one(COL, snapshot).await.map(|_| ()) } - async fn fetch_snapshot(&self, report_id: &str) -> Result { - self.find_one( + async fn fetch_snapshots(&self, report_id: &str) -> Result> { + self.find( COL, doc! { "report_id": report_id diff --git a/crates/quark/src/traits/safety/snapshot.rs b/crates/quark/src/traits/safety/snapshot.rs index 2c1e4d9c..516ef87d 100644 --- a/crates/quark/src/traits/safety/snapshot.rs +++ b/crates/quark/src/traits/safety/snapshot.rs @@ -6,6 +6,6 @@ pub trait AbstractSnapshot: Sync + Send { /// Insert a new snapshot into the database async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>; - /// Fetch a snapshot by a report's id - async fn fetch_snapshot(&self, report_id: &str) -> Result; + /// Fetch a snapshots by a report's id + async fn fetch_snapshots(&self, report_id: &str) -> Result>; }