feat: generate and fetch multiple snapshots

This commit is contained in:
Paul Makles
2023-05-21 15:36:36 +01:00
parent d5f903781d
commit a77b7717b8
9 changed files with 220 additions and 165 deletions

View File

@@ -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/<report_id>")]
pub async fn fetch_snapshot(
db: &Db,
user: User,
report_id: String,
) -> Result<Json<SnapshotWithContext>> {
// 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<String> = user_ids.into_iter().map(|s| s.to_owned()).collect();
let channel_ids: Vec<String> = 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,
}))
}

View File

@@ -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/<report_id>")]
pub async fn fetch_snapshots(
db: &Db,
user: User,
report_id: String,
) -> Result<Json<Vec<SnapshotWithContext>>> {
// 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<String> = user_ids.into_iter().map(|s| s.to_owned()).collect();
let channel_ids: Vec<String> = 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))
}

View File

@@ -6,7 +6,7 @@ mod fetch_report;
mod fetch_reports;
mod report_content;
mod fetch_snapshot;
mod fetch_snapshots;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
@@ -16,6 +16,6 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
fetch_reports::fetch_reports,
report_content::report_content,
// Snapshots
fetch_snapshot::fetch_snapshot
fetch_snapshots::fetch_snapshots
]
}

View File

@@ -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<DataReportContent>)
// Find the content and create a snapshot of it
// Also retrieve any references to Files
let (content, files): (SnapshotContent, Vec<String>) = match &data.content {
let (snapshots, files): (Vec<SnapshotContent>, Vec<String>) = 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<DataReportContent>)
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<DataReportContent>)
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<DataReportContent>)
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<DataReportContent>)
// 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 {

View File

@@ -10,7 +10,7 @@ impl AbstractSnapshot for DummyDb {
Ok(())
}
async fn fetch_snapshot(&self, _report_id: &str) -> Result<Snapshot> {
async fn fetch_snapshots(&self, _report_id: &str) -> Result<Vec<Snapshot>> {
todo!()
}
}

View File

@@ -26,4 +26,5 @@ pub mod users {
pub mod safety {
pub mod report;
pub mod snapshot;
}

View File

@@ -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<String>)> {
// 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<String>)> {
// 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<String>)> {
// 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))
}
}

View File

@@ -11,8 +11,8 @@ impl AbstractSnapshot for MongoDb {
self.insert_one(COL, snapshot).await.map(|_| ())
}
async fn fetch_snapshot(&self, report_id: &str) -> Result<Snapshot> {
self.find_one(
async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>> {
self.find(
COL,
doc! {
"report_id": report_id

View File

@@ -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<Snapshot>;
/// Fetch a snapshots by a report's id
async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>>;
}