Compare commits

...
6 Commits
27 changed files with 421 additions and 21 deletions
Generated
+4 -4
View File
@@ -2170,7 +2170,7 @@ dependencies = [
[[package]]
name = "optional_struct"
version = "0.2.0"
source = "git+https://github.com/insertish/OptionalStruct?rev=e275d2726595474632485934aa0887fa52281f70#e275d2726595474632485934aa0887fa52281f70"
source = "git+https://github.com/insertish/OptionalStruct?rev=ee56427cee1f007839825d93d07fffd5a5e038c7#ee56427cee1f007839825d93d07fffd5a5e038c7"
dependencies = [
"quote 0.3.15",
"syn 0.11.11",
@@ -2792,7 +2792,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "0.5.10"
version = "0.5.14"
dependencies = [
"async-std",
"async-tungstenite",
@@ -2808,7 +2808,7 @@ dependencies = [
[[package]]
name = "revolt-delta"
version = "0.5.10"
version = "0.5.14"
dependencies = [
"async-channel",
"async-std",
@@ -2848,7 +2848,7 @@ dependencies = [
[[package]]
name = "revolt-quark"
version = "0.5.10"
version = "0.5.14"
dependencies = [
"async-lock",
"async-recursion",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.5.10"
version = "0.5.14"
license = "AGPL-3.0-or-later"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.5.10"
version = "0.5.14"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -1,8 +1,9 @@
use revolt_quark::{
models::message::{PartialMessage, SendableEmbed},
models::{Message, User},
perms,
types::january::Embed,
Db, Error, Ref, Result, Timestamp,
Db, Error, Permission, Ref, Result, Timestamp,
};
use rocket::serde::json::Json;
@@ -28,7 +29,7 @@ pub struct DataEditMessage {
pub async fn req(
db: &Db,
user: User,
target: String,
target: Ref,
msg: Ref,
edit: Json<DataEditMessage>,
) -> Result<Json<Message>> {
@@ -36,10 +37,17 @@ pub async fn req(
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
// Ensure we have permissions to send a message
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::SendMessage)
.await?;
Message::validate_sum(&edit.content, &edit.embeds)?;
let mut message = msg.as_message(db).await?;
if message.channel != target {
if message.channel != channel.id() {
return Err(Error::NotFound);
}
+2 -1
View File
@@ -12,6 +12,7 @@ mod push;
mod root;
mod safety;
mod servers;
mod stats;
mod sync;
mod users;
@@ -21,7 +22,7 @@ pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
mount_endpoints_and_merged_docs! {
rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root, root::ping],
"" => openapi_get_routes_spec![root::root, stats::stats, root::ping],
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
@@ -0,0 +1,56 @@
use revolt_quark::{
models::{
report::{PartialReport, ReportStatus},
Report, User,
},
Db, Error, Ref, Result,
};
use rocket::serde::json::Json;
use serde::Deserialize;
use validator::Validate;
/// # Report Data
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataEditReport {
/// New report status
status: Option<ReportStatus>,
/// Report notes
notes: Option<String>,
}
/// # Edit Report
///
/// Edit a report.
#[openapi(tag = "User Safety")]
#[patch("/reports/<report>", data = "<edit>")]
pub async fn edit_report(
db: &Db,
user: User,
report: Ref,
edit: Json<DataEditReport>,
) -> Result<Json<Report>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
// Validate data
let edit = edit.into_inner();
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
// Create and apply update to report
let mut report = report.as_report(db).await?;
report
.update(
db,
PartialReport {
status: edit.status,
notes: edit.notes,
..Default::default()
},
)
.await?;
Ok(Json(report))
}
@@ -0,0 +1,17 @@
use revolt_quark::models::{Report, User};
use revolt_quark::{Db, Error, Result};
use rocket::serde::json::Json;
/// # Fetch Report
///
/// Fetch a report by its ID
#[openapi(tag = "User Safety")]
#[get("/report/<id>")]
pub async fn fetch_report(db: &Db, user: User, id: String) -> Result<Json<Report>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
db.fetch_report(&id).await.map(Json)
}
@@ -44,8 +44,13 @@ pub async fn fetch_snapshot(
user_ids.insert(&message.author);
channel_ids.insert(&message.channel);
}
_ => {
todo!()
SnapshotContent::User(user) => {
user_ids.insert(&user.id);
}
SnapshotContent::Server(server) => {
for channel in &server.channels {
channel_ids.insert(channel);
}
}
}
+5 -1
View File
@@ -1,6 +1,8 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod edit_report;
mod fetch_report;
mod fetch_reports;
mod report_content;
@@ -9,8 +11,10 @@ mod fetch_snapshot;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
// Reports
report_content::report_content,
edit_report::edit_report,
fetch_report::fetch_report,
fetch_reports::fetch_reports,
report_content::report_content,
// Snapshots
fetch_snapshot::fetch_snapshot
]
@@ -149,7 +149,8 @@ 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,
status: ReportStatus::Created {},
notes: String::new(),
};
db.insert_report(&report).await?;
+13
View File
@@ -0,0 +1,13 @@
use revolt_quark::models::stats::Stats;
use revolt_quark::{Db, Result};
use rocket::serde::json::Json;
/// # Query Stats
///
/// Fetch various technical statistics.
#[openapi(tag = "Core")]
#[get("/stats")]
pub async fn stats(db: &Db) -> Result<Json<Stats>> {
Ok(Json(db.generate_stats().await?))
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-quark"
version = "0.5.10"
version = "0.5.14"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -29,7 +29,7 @@ default = [ "test" ]
serde = { version = "1", features = ["derive"] }
validator = { version = "0.14", features = ["derive"] }
iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] }
optional_struct = { git = "https://github.com/insertish/OptionalStruct", rev = "e275d2726595474632485934aa0887fa52281f70" }
optional_struct = { git = "https://github.com/insertish/OptionalStruct", rev = "ee56427cee1f007839825d93d07fffd5a5e038c7" }
# Formats
bincode = "1.3.3"
@@ -0,0 +1,10 @@
use crate::{models::stats::Stats, AbstractStats, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractStats for DummyDb {
async fn generate_stats(&self) -> Result<Stats> {
todo!()
}
}
+1
View File
@@ -2,6 +2,7 @@ use crate::AbstractDatabase;
pub mod admin {
pub mod migrations;
pub mod stats;
}
pub mod media {
@@ -1,3 +1,4 @@
use crate::models::report::PartialReport;
use crate::models::Report;
use crate::{AbstractReport, Result};
@@ -10,6 +11,14 @@ impl AbstractReport for DummyDb {
Ok(())
}
async fn update_report(&self, _id: &str, _report: &PartialReport) -> Result<()> {
todo!()
}
async fn fetch_report(&self, _report_id: &str) -> Result<Report> {
todo!()
}
async fn fetch_reports(&self) -> Result<Vec<Report>> {
Ok(vec![])
}
+4
View File
@@ -27,3 +27,7 @@ pub mod users {
pub mod user;
pub mod user_settings;
}
pub mod safety {
pub mod report;
}
@@ -0,0 +1,9 @@
use crate::{models::report::PartialReport, models::Report, Database, Result};
impl Report {
/// Update report data
pub async fn update(&mut self, db: &Database, partial: PartialReport) -> Result<()> {
self.apply_options(partial.clone());
db.update_report(&self.id, &partial).await
}
}
@@ -0,0 +1,90 @@
use std::collections::HashMap;
use bson::{from_document, Document};
use futures::StreamExt;
use crate::{
models::stats::{Index, Stats},
AbstractStats, Error, Result,
};
use super::super::MongoDb;
#[async_trait]
impl AbstractStats for MongoDb {
async fn generate_stats(&self) -> Result<Stats> {
let mut indices = HashMap::new();
let mut coll_stats = HashMap::new();
let collection_names =
self.db()
.list_collection_names(None)
.await
.map_err(|_| Error::DatabaseError {
operation: "list_collection_names",
with: "database",
})?;
for collection in collection_names {
indices.insert(
collection.to_string(),
self.col::<Document>(&collection)
.aggregate(
vec![doc! {
"$indexStats": { }
}],
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "aggregate",
with: "col",
})?
.filter_map(|s| async { s.ok() })
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|doc| from_document(doc).ok())
.collect::<Vec<Index>>(),
);
coll_stats.insert(
collection.to_string(),
self.col::<Document>(&collection)
.aggregate(
vec![doc! {
"$collStats": {
"latencyStats": {
"histograms": true
},
"storageStats": {},
"count": {},
"queryExecStats": {}
}
}],
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "aggregate",
with: "col",
})?
.filter_map(|s| async { s.ok() })
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|doc| from_document(doc).ok())
.next()
.ok_or(Error::DatabaseError {
operation: "next aggregation",
with: "col",
})?,
);
}
Ok(Stats {
indices,
coll_stats,
})
}
}
+9 -1
View File
@@ -13,6 +13,7 @@ use crate::{util::manipulation::prefix_keys, AbstractDatabase, Error, Result};
pub mod admin {
pub mod migrations;
pub mod stats;
}
pub mod media {
@@ -97,7 +98,14 @@ impl MongoDb {
operation: "find",
with: collection,
})?
.filter_map(|s| async { s.ok() })
.filter_map(|s| async {
if cfg!(debug_assertions) {
// Hard fail on invalid documents
Some(s.unwrap())
} else {
s.ok()
}
})
.collect::<Vec<T>>()
.await)
}
@@ -1,3 +1,4 @@
use crate::models::report::PartialReport;
use crate::models::Report;
use crate::{AbstractReport, Result};
@@ -11,6 +12,16 @@ impl AbstractReport for MongoDb {
self.insert_one(COL, report).await.map(|_| ())
}
async fn update_report(&self, id: &str, report: &PartialReport) -> Result<()> {
self.update_one_by_id(COL, id, report, vec![], None)
.await
.map(|_| ())
}
async fn fetch_report(&self, report_id: &str) -> Result<Report> {
self.find_one_by_id(COL, report_id).await
}
async fn fetch_reports(&self) -> Result<Vec<Report>> {
self.find(COL, doc! {}).await
}
+122
View File
@@ -0,0 +1,122 @@
use std::collections::HashMap;
use iso8601_timestamp::Timestamp;
use serde::{Deserialize, Serialize};
/// Index access information
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct IndexAccess {
/// Operations since timestamp
ops: i32,
/// Timestamp at which data keeping begun
since: Timestamp,
}
/// Collection index
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct Index {
/// Index name
name: String,
/// Access information
accesses: IndexAccess,
}
/// Histogram entry
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct LatencyHistogramEntry {
/// Time
micros: i64,
/// Count
count: i64,
}
/// Collection latency stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct LatencyStats {
/// Total operations
ops: i64,
/// Timestamp at which data keeping begun
latency: i64,
/// Histogram representation of latency data
histogram: Vec<LatencyHistogramEntry>,
}
/// Collection storage stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StorageStats {
/// Uncompressed data size
size: i64,
/// Data size on disk
storage_size: i64,
/// Total size of all indexes
total_index_size: i64,
/// Sum of storage size and total index size
total_size: i64,
/// Individual index sizes
index_sizes: HashMap<String, i64>,
/// Number of documents in collection
count: i64,
/// Average size of each document
avg_obj_size: i64,
}
/// Query collection scan stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CollectionScans {
/// Number of total collection scans
total: i64,
/// Number of total collection scans not using a tailable cursor
non_tailable: i64,
}
/// Collection query execution stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueryExecStats {
/// Stats regarding collection scans
collection_scans: CollectionScans,
}
/// Collection stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CollectionStats {
/// Namespace
ns: String,
/// Local time
local_time: Timestamp,
/// Latency stats
latency_stats: HashMap<String, LatencyStats>,
/// Query exec stats
query_exec_stats: QueryExecStats,
/// Number of documents in collection
count: u64,
}
/// Server Stats
#[derive(Serialize, JsonSchema, Debug)]
pub struct Stats {
/// Index usage information
pub indices: HashMap<String, Vec<Index>>,
/// Collection stats
pub coll_stats: HashMap<String, CollectionStats>,
}
+1
View File
@@ -1,6 +1,7 @@
mod admin {
pub mod migrations;
pub mod simple;
pub mod stats;
}
mod media {
+10 -3
View File
@@ -76,17 +76,20 @@ pub enum ReportedContent {
#[serde(tag = "status")]
pub enum ReportStatus {
/// Report is waiting for triage / action
Created,
Created {},
/// Report was rejected
Rejected { rejection_reason: String },
/// Report was actioned and resolved
Resolved,
Resolved {},
}
/// User-generated platform moderation report.
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, OptionalStruct, Clone)]
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
#[optional_name = "PartialReport"]
#[opt_skip_serializing_none]
pub struct Report {
/// Unique Id
#[serde(rename = "_id")]
@@ -98,6 +101,10 @@ pub struct Report {
/// Additional report context
pub additional_context: String,
/// Status of the report
#[opt_passthrough]
#[serde(flatten)]
pub status: ReportStatus,
/// Additional notes included on the report
#[serde(default)]
pub notes: String,
}
+6
View File
@@ -0,0 +1,6 @@
use crate::{models::stats::Stats, Result};
#[async_trait]
pub trait AbstractStats: Sync + Send {
async fn generate_stats(&self) -> Result<Stats>;
}
+3
View File
@@ -1,5 +1,6 @@
mod admin {
pub mod migrations;
pub mod stats;
}
mod media {
@@ -32,6 +33,7 @@ mod safety {
}
pub use admin::migrations::AbstractMigrations;
pub use admin::stats::AbstractStats;
pub use media::attachment::AbstractAttachment;
pub use media::emoji::AbstractEmoji;
@@ -56,6 +58,7 @@ pub trait AbstractDatabase:
Sync
+ Send
+ AbstractMigrations
+ AbstractStats
+ AbstractAttachment
+ AbstractEmoji
+ AbstractChannel
+7
View File
@@ -1,3 +1,4 @@
use crate::models::report::PartialReport;
use crate::models::Report;
use crate::Result;
@@ -6,6 +7,12 @@ pub trait AbstractReport: Sync + Send {
/// Insert a new report into the database
async fn insert_report(&self, report: &Report) -> Result<()>;
/// Update a given report with new information
async fn update_report(&self, id: &str, message: &PartialReport) -> Result<()>;
/// Fetch report
async fn fetch_report(&self, report_id: &str) -> Result<Report>;
/// Fetch reports
async fn fetch_reports(&self) -> Result<Vec<Report>>;
}
+8 -1
View File
@@ -4,7 +4,9 @@ use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::models::{Bot, Channel, Emoji, Invite, Member, Message, Server, ServerBan, User};
use crate::models::{
Bot, Channel, Emoji, Invite, Member, Message, Report, Server, ServerBan, User,
};
use crate::presence::presence_is_online;
use crate::{Database, Error, Result};
@@ -78,6 +80,11 @@ impl Ref {
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
db.fetch_emoji(&self.id).await
}
/// Fetch report from Ref
pub async fn as_report(&self, db: &Database) -> Result<Report> {
db.fetch_report(&self.id).await
}
}
impl<'r> FromParam<'r> for Ref {