Compare commits

...
4 Commits
19 changed files with 289 additions and 18 deletions

No files matched your search

Generated
+3 -3
View File
@@ -2792,7 +2792,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "0.5.9"
version = "0.5.10"
dependencies = [
"async-std",
"async-tungstenite",
@@ -2808,7 +2808,7 @@ dependencies = [
[[package]]
name = "revolt-delta"
version = "0.5.9"
version = "0.5.10"
dependencies = [
"async-channel",
"async-std",
@@ -2848,7 +2848,7 @@ dependencies = [
[[package]]
name = "revolt-quark"
version = "0.5.9"
version = "0.5.10"
dependencies = [
"async-lock",
"async-recursion",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.5.9"
version = "0.5.10"
license = "AGPL-3.0-or-later"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.5.9"
version = "0.5.10"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -0,0 +1,17 @@
use revolt_quark::models::{Report, User};
use revolt_quark::{Db, Error, Result};
use rocket::serde::json::Json;
/// # Fetch Reports
///
/// Fetch all available reports
#[openapi(tag = "User Safety")]
#[get("/reports")]
pub async fn fetch_reports(db: &Db, user: User) -> Result<Json<Vec<Report>>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
db.fetch_reports().await.map(Json)
}
@@ -0,0 +1,77 @@
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);
}
_ => {
todo!()
}
}
// 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,
}))
}
+7 -1
View File
@@ -1,11 +1,17 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod fetch_reports;
mod report_content;
mod fetch_snapshot;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
// Reports
report_content::report_content
report_content::report_content,
fetch_reports::fetch_reports,
// Snapshots
fetch_snapshot::fetch_snapshot
]
}
@@ -1,5 +1,5 @@
use revolt_quark::events::client::EventV1;
use revolt_quark::models::report::ReportedContent;
use revolt_quark::models::report::{ReportStatus, ReportedContent};
use revolt_quark::models::snapshot::{Snapshot, SnapshotContent};
use revolt_quark::models::{Report, User};
use revolt_quark::{Db, Error, Result};
@@ -149,6 +149,7 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
author_id: user.id,
content: data.content,
additional_context: data.additional_context,
status: ReportStatus::Created,
};
db.insert_report(&report).await?;
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-quark"
version = "0.5.9"
version = "0.5.10"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -9,4 +9,8 @@ impl AbstractReport for DummyDb {
info!("Insert {:?}", report);
Ok(())
}
async fn fetch_reports(&self) -> Result<Vec<Report>> {
Ok(vec![])
}
}
@@ -9,4 +9,8 @@ impl AbstractSnapshot for DummyDb {
info!("Insert {:?}", snapshot);
Ok(())
}
async fn fetch_snapshot(&self, _report_id: &str) -> Result<Snapshot> {
todo!()
}
}
@@ -57,6 +57,14 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create user_settings collection.");
db.create_collection("safety_reports", None)
.await
.expect("Failed to create safety_reports collection.");
db.create_collection("safety_snapshots", None)
.await
.expect("Failed to create safety_snapshots collection.");
db.create_collection("bots", None)
.await
.expect("Failed to create bots collection.");
@@ -103,18 +111,18 @@ pub async fn create_database(db: &MongoDb) {
},
"name": "content"
},
{
"key": {
"channel": 1_i32
},
"name": "channel"
},
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
},
{
"key": {
"author": 1_i32
},
"name": "author"
}
]
},
@@ -16,7 +16,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 18;
pub const LATEST_REVISION: i32 = 21;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -661,6 +661,103 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to update server members.");
}
if revision <= 18 {
info!("Running migration [revision 18 / 27-02-2022]: Create author index on messages. Drop plain channel index if exists.");
if db
.db()
.run_command(
doc! {
"dropIndexes": "messages",
"index": ["channel"]
},
None,
)
.await
.is_err()
{
info!("Failed to drop `messages.channel` index but this is ok since that means it's probably gone.");
}
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"author": 1_i32,
},
"name": "author"
}
]
},
None,
)
.await
.expect("Failed to create messages author index.");
}
if revision <= 19 {
info!("Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections, migrate to new model if applicable.");
// TODO: make these fail once production is migrated
if db
.db()
.create_collection("safety_reports", None)
.await
.is_err()
{
info!("Failed to create safety_reports collection but this is expected in production.");
}
if db
.db()
.create_collection("safety_snapshots", None)
.await
.is_err()
{
info!(
"Failed to create safety_snapshots collection but this is expected in production."
);
}
db.col::<Document>("safety_reports")
.update_many(
doc! {},
doc! {
"$set": {
"status": "Created"
}
},
None,
)
.await
.unwrap();
}
if revision <= 20 {
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
db.db()
.run_command(
doc! {
"createIndexes": "safety_snapshots",
"indexes": [
{
"key": {
"report_id": 1_i32
},
"name": "report_id"
}
]
},
None,
)
.await
.expect("Failed to create safety snapshot index.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations.
@@ -10,4 +10,8 @@ impl AbstractReport for MongoDb {
async fn insert_report(&self, report: &Report) -> Result<()> {
self.insert_one(COL, report).await.map(|_| ())
}
async fn fetch_reports(&self) -> Result<Vec<Report>> {
self.find(COL, doc! {}).await
}
}
@@ -10,4 +10,14 @@ impl AbstractSnapshot for MongoDb {
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
self.insert_one(COL, snapshot).await.map(|_| ())
}
async fn fetch_snapshot(&self, report_id: &str) -> Result<Snapshot> {
self.find_one(
COL,
doc! {
"report_id": report_id
},
)
.await
}
}
+18
View File
@@ -44,6 +44,7 @@ pub enum UserReportReason {
Underage,
}
/// The content being reported
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[serde(tag = "type")]
pub enum ReportedContent {
@@ -70,6 +71,20 @@ pub enum ReportedContent {
},
}
/// Status of the report
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[serde(tag = "status")]
pub enum ReportStatus {
/// Report is waiting for triage / action
Created,
/// Report was rejected
Rejected { rejection_reason: String },
/// Report was actioned and resolved
Resolved,
}
/// User-generated platform moderation report.
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
pub struct Report {
@@ -82,4 +97,7 @@ pub struct Report {
pub content: ReportedContent,
/// Additional report context
pub additional_context: String,
/// Status of the report
#[serde(flatten)]
pub status: ReportStatus,
}
+20 -3
View File
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::models::{Message, Server, User};
use crate::models::{Channel, Message, Server, User};
/// Enum to map into different models
/// that can be saved in a snapshot
@@ -9,11 +9,11 @@ use crate::models::{Message, Server, User};
pub enum SnapshotContent {
Message {
/// Context before the message
#[serde(rename = "_prior_context")]
#[serde(rename = "_prior_context", default)]
prior_context: Vec<Message>,
/// Context after the message
#[serde(rename = "_leading_context")]
#[serde(rename = "_leading_context", default)]
leading_context: Vec<Message>,
/// Message
@@ -35,3 +35,20 @@ pub struct Snapshot {
/// Snapshot of content
pub content: SnapshotContent,
}
/// Snapshot of some content with required data to render
#[derive(Serialize, JsonSchema, Debug)]
pub struct SnapshotWithContext {
/// Snapshot itself
#[serde(flatten)]
pub snapshot: Snapshot,
/// Users involved in snapshot
#[serde(rename = "_users", skip_serializing_if = "Vec::is_empty")]
pub users: Vec<User>,
/// Channels involved in snapshot
#[serde(rename = "_channels", skip_serializing_if = "Vec::is_empty")]
pub channels: Vec<Channel>,
/// Server involved in snapshot
#[serde(rename = "_server", skip_serializing_if = "Option::is_none")]
pub server: Option<Server>,
}
+3
View File
@@ -5,4 +5,7 @@ use crate::Result;
pub trait AbstractReport: Sync + Send {
/// Insert a new report into the database
async fn insert_report(&self, report: &Report) -> Result<()>;
/// Fetch reports
async fn fetch_reports(&self) -> Result<Vec<Report>>;
}
@@ -5,4 +5,7 @@ use crate::Result;
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>;
}
+2
View File
@@ -75,6 +75,7 @@ pub enum Error {
permission: UserPermission,
},
NotElevated,
NotPrivileged,
CannotGiveMissingPermissions,
NotOwner,
@@ -174,6 +175,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::MissingPermission { .. } => Status::Forbidden,
Error::MissingUserPermission { .. } => Status::Forbidden,
Error::NotElevated => Status::Forbidden,
Error::NotPrivileged => Status::Forbidden,
Error::CannotGiveMissingPermissions => Status::Forbidden,
Error::NotOwner => Status::Forbidden,