diff --git a/Cargo.lock b/Cargo.lock index 7f795004..ae1e92d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2106,9 +2106,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "opaque-debug" @@ -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.9" +version = "0.5.17" dependencies = [ "async-std", "async-tungstenite", @@ -2808,7 +2808,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.5.9" +version = "0.5.17" dependencies = [ "async-channel", "async-std", @@ -2819,7 +2819,6 @@ dependencies = [ "env_logger", "futures", "impl_ops", - "lazy_static", "lettre", "linkify 0.6.0", "log", @@ -2848,7 +2847,7 @@ dependencies = [ [[package]] name = "revolt-quark" -version = "0.5.9" +version = "0.5.17" dependencies = [ "async-lock", "async-recursion", @@ -2866,7 +2865,6 @@ dependencies = [ "impl_ops", "indexmap", "iso8601-timestamp", - "lazy_static", "linkify 0.8.1", "log", "lru", diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index 2e36f6fc..9aaf7c72 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.5.9" +version = "0.5.17" license = "AGPL-3.0-or-later" edition = "2021" diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index abd3923d..c488923d 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.5.9" +version = "0.5.17" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" @@ -15,9 +15,8 @@ log = "0.4.11" dotenv = "0.15.0" dashmap = "5.2.0" linkify = "0.6.0" -once_cell = "1.4.1" +once_cell = "1.17.1" env_logger = "0.7.1" -lazy_static = "1.4.0" # Lang. Utilities regex = "1" diff --git a/crates/delta/build.rs b/crates/delta/build.rs index 981237b4..098d7b25 100644 --- a/crates/delta/build.rs +++ b/crates/delta/build.rs @@ -8,7 +8,7 @@ fn main() { .output() { if let Ok(git_origin) = String::from_utf8(output.stdout) { - println!("cargo:rustc-env=GIT_ORIGIN_URL={}", git_origin); + println!("cargo:rustc-env=GIT_ORIGIN_URL={git_origin}"); } } diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 255fbb0a..f8d1b20f 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -4,8 +4,6 @@ extern crate rocket; extern crate revolt_rocket_okapi; #[macro_use] extern crate serde_json; -#[macro_use] -extern crate lazy_static; pub mod routes; pub mod util; diff --git a/crates/delta/src/routes/admin/message_query.rs b/crates/delta/src/routes/admin/message_query.rs new file mode 100644 index 00000000..e9b1a1e5 --- /dev/null +++ b/crates/delta/src/routes/admin/message_query.rs @@ -0,0 +1,31 @@ +use revolt_quark::{ + models::{ + message::{BulkMessageResponse, MessageQuery}, + User, + }, + Db, Error, Result, +}; +use rocket::serde::json::Json; + +/// # Globally Fetch Messages +/// +/// This is a privileged route to globally fetch messages. +#[openapi(tag = "Admin")] +#[post("/messages", data = "")] +pub async fn message_query( + db: &Db, + user: User, + data: Json, +) -> Result> { + // Must be privileged for this route + if !user.privileged { + return Err(Error::NotPrivileged); + } + + // Fetch data using query + let data = data.into_inner(); + let messages = db.fetch_messages(data).await?; + BulkMessageResponse::transform(db, None, messages, Some(true)) + .await + .map(Json) +} diff --git a/crates/delta/src/routes/admin/mod.rs b/crates/delta/src/routes/admin/mod.rs new file mode 100644 index 00000000..28baf710 --- /dev/null +++ b/crates/delta/src/routes/admin/mod.rs @@ -0,0 +1,9 @@ +use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; +use rocket::Route; + +mod message_query; +mod stats; + +pub fn routes() -> (Vec, OpenApi) { + openapi_get_routes_spec![stats::stats, message_query::message_query] +} diff --git a/crates/delta/src/routes/admin/stats.rs b/crates/delta/src/routes/admin/stats.rs new file mode 100644 index 00000000..1d043b03 --- /dev/null +++ b/crates/delta/src/routes/admin/stats.rs @@ -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 = "Admin")] +#[get("/stats")] +pub async fn stats(db: &Db) -> Result> { + Ok(Json(db.generate_stats().await?)) +} diff --git a/crates/delta/src/routes/channels/group_add_member.rs b/crates/delta/src/routes/channels/group_add_member.rs index a7f8096d..b1dc9c03 100644 --- a/crates/delta/src/routes/channels/group_add_member.rs +++ b/crates/delta/src/routes/channels/group_add_member.rs @@ -1,5 +1,6 @@ use revolt_quark::{ - models::{Channel, User}, + get_relationship, + models::{user::RelationshipStatus, Channel, User}, perms, Db, EmptyResponse, Error, Permission, Ref, Result, }; @@ -22,6 +23,13 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result { let member = member.as_user(db).await?; + if !matches!( + get_relationship(&user, &member.id), + RelationshipStatus::Friend + ) { + return Err(Error::NotFriends); + } + channel .add_user_to_group(db, &member.id, &user.id) .await diff --git a/crates/delta/src/routes/channels/message_edit.rs b/crates/delta/src/routes/channels/message_edit.rs index d9388c6e..228bbb87 100644 --- a/crates/delta/src/routes/channels/message_edit.rs +++ b/crates/delta/src/routes/channels/message_edit.rs @@ -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, ) -> Result> { @@ -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); } diff --git a/crates/delta/src/routes/channels/message_query.rs b/crates/delta/src/routes/channels/message_query.rs index a6dfe267..fba69bdb 100644 --- a/crates/delta/src/routes/channels/message_query.rs +++ b/crates/delta/src/routes/channels/message_query.rs @@ -1,6 +1,8 @@ use revolt_quark::{ models::{ - message::{BulkMessageResponse, MessageSort}, + message::{ + BulkMessageResponse, MessageFilter, MessageQuery, MessageSort, MessageTimePeriod, + }, User, }, perms, Db, Error, Permission, Ref, Result, @@ -68,14 +70,28 @@ pub async fn req( sort, nearby, include_users, - .. } = options; let messages = db - .fetch_messages(channel.id(), limit, before, after, sort, nearby) + .fetch_messages(MessageQuery { + filter: MessageFilter { + channel: Some(channel.id().to_string()), + ..Default::default() + }, + time_period: if let Some(nearby) = nearby { + MessageTimePeriod::Relative { nearby } + } else { + MessageTimePeriod::Absolute { + before, + after, + sort, + } + }, + limit, + }) .await?; - BulkMessageResponse::transform(db, &channel, messages, include_users) + BulkMessageResponse::transform(db, Some(&channel), messages, include_users) .await .map(Json) } diff --git a/crates/delta/src/routes/channels/message_search.rs b/crates/delta/src/routes/channels/message_search.rs index 0db35568..37f32e30 100644 --- a/crates/delta/src/routes/channels/message_search.rs +++ b/crates/delta/src/routes/channels/message_search.rs @@ -1,6 +1,8 @@ use revolt_quark::{ models::{ - message::{BulkMessageResponse, MessageSort}, + message::{ + BulkMessageResponse, MessageFilter, MessageQuery, MessageSort, MessageTimePeriod, + }, User, }, perms, Db, Error, Permission, Ref, Result, @@ -30,7 +32,7 @@ pub struct OptionsMessageSearch { after: Option, /// Message sort direction /// - /// By default, it will be sorted by relevance. + /// By default, it will be sorted by latest. #[serde(default = "MessageSort::default")] sort: MessageSort, /// Whether to include user (and member, if server channel) objects @@ -73,10 +75,22 @@ pub async fn req( } = options; let messages = db - .search_messages(channel.id(), &query, limit, before, after, sort) + .fetch_messages(MessageQuery { + filter: MessageFilter { + channel: Some(channel.id().to_string()), + query: Some(query), + ..Default::default() + }, + time_period: MessageTimePeriod::Absolute { + before, + after, + sort: Some(sort), + }, + limit, + }) .await?; - BulkMessageResponse::transform(db, &channel, messages, include_users) + BulkMessageResponse::transform(db, Some(&channel), messages, include_users) .await .map(Json) } diff --git a/crates/delta/src/routes/channels/message_send.rs b/crates/delta/src/routes/channels/message_send.rs index 9645fffc..76cfc40a 100644 --- a/crates/delta/src/routes/channels/message_send.rs +++ b/crates/delta/src/routes/channels/message_send.rs @@ -10,6 +10,7 @@ use revolt_quark::{ use rocket::serde::json::Json; use validator::Validate; +use once_cell::sync::Lazy; /// # Send Message /// diff --git a/crates/delta/src/routes/mod.rs b/crates/delta/src/routes/mod.rs index 6b957f5f..98f7aee7 100644 --- a/crates/delta/src/routes/mod.rs +++ b/crates/delta/src/routes/mod.rs @@ -3,6 +3,7 @@ pub use rocket::http::Status; pub use rocket::response::Redirect; use rocket::{Build, Rocket}; +mod admin; mod bots; mod channels; mod customisation; @@ -23,6 +24,7 @@ pub fn mount(mut rocket: Rocket) -> Rocket { rocket, "/".to_owned(), settings, "/" => (vec![], custom_openapi_spec()), "" => openapi_get_routes_spec![root::root, root::ping], + "/admin" => admin::routes(), "/users" => users::routes(), "/bots" => bots::routes(), "/channels" => channels::routes(), @@ -111,8 +113,9 @@ fn custom_openapi_spec() -> OpenApi { ] }, { - "name": "Platform Moderation", + "name": "Platform Administration", "tags": [ + "Admin", "User Safety" ] }, diff --git a/crates/delta/src/routes/safety/edit_report.rs b/crates/delta/src/routes/safety/edit_report.rs new file mode 100644 index 00000000..eb7103d0 --- /dev/null +++ b/crates/delta/src/routes/safety/edit_report.rs @@ -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, + /// Report notes + notes: Option, +} + +/// # Edit Report +/// +/// Edit a report. +#[openapi(tag = "User Safety")] +#[patch("/reports/", data = "")] +pub async fn edit_report( + db: &Db, + user: User, + report: Ref, + edit: Json, +) -> Result> { + // 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)) +} diff --git a/crates/delta/src/routes/safety/fetch_report.rs b/crates/delta/src/routes/safety/fetch_report.rs new file mode 100644 index 00000000..e5d84bbc --- /dev/null +++ b/crates/delta/src/routes/safety/fetch_report.rs @@ -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/")] +pub async fn fetch_report(db: &Db, user: User, id: String) -> Result> { + // Must be privileged for this route + if !user.privileged { + return Err(Error::NotPrivileged); + } + + db.fetch_report(&id).await.map(Json) +} diff --git a/crates/delta/src/routes/safety/fetch_reports.rs b/crates/delta/src/routes/safety/fetch_reports.rs new file mode 100644 index 00000000..5b418472 --- /dev/null +++ b/crates/delta/src/routes/safety/fetch_reports.rs @@ -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>> { + // Must be privileged for this route + if !user.privileged { + return Err(Error::NotPrivileged); + } + + db.fetch_reports().await.map(Json) +} diff --git a/crates/delta/src/routes/safety/fetch_snapshot.rs b/crates/delta/src/routes/safety/fetch_snapshot.rs new file mode 100644 index 00000000..7d32bcf2 --- /dev/null +++ b/crates/delta/src/routes/safety/fetch_snapshot.rs @@ -0,0 +1,82 @@ +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/mod.rs b/crates/delta/src/routes/safety/mod.rs index 29102aa6..7aee1e8a 100644 --- a/crates/delta/src/routes/safety/mod.rs +++ b/crates/delta/src/routes/safety/mod.rs @@ -1,11 +1,21 @@ use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; use rocket::Route; +mod edit_report; +mod fetch_report; +mod fetch_reports; mod report_content; +mod fetch_snapshot; + pub fn routes() -> (Vec, 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 ] } diff --git a/crates/delta/src/routes/safety/report_content.rs b/crates/delta/src/routes/safety/report_content.rs index 295b4e2b..5a7060b7 100644 --- a/crates/delta/src/routes/safety/report_content.rs +++ b/crates/delta/src/routes/safety/report_content.rs @@ -1,5 +1,6 @@ use revolt_quark::events::client::EventV1; -use revolt_quark::models::report::ReportedContent; +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}; use revolt_quark::{Db, Error, Result}; @@ -55,26 +56,34 @@ pub async fn report_content(db: &Db, user: User, data: Json) // Collect prior context let prior_context = db - .fetch_messages( - &message.channel, - Some(15), - Some(message.id.to_string()), - None, - None, - None, - ) + .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( - &message.channel, - Some(15), - None, - Some(message.id.to_string()), - None, - None, - ) + .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?; ( @@ -149,6 +158,8 @@ pub async fn report_content(db: &Db, user: User, data: Json) author_id: user.id, content: data.content, additional_context: data.additional_context, + status: ReportStatus::Created {}, + notes: String::new(), }; db.insert_report(&report).await?; diff --git a/crates/delta/src/routes/servers/channel_create.rs b/crates/delta/src/routes/servers/channel_create.rs index aff4f964..1c777639 100644 --- a/crates/delta/src/routes/servers/channel_create.rs +++ b/crates/delta/src/routes/servers/channel_create.rs @@ -11,20 +11,15 @@ use ulid::Ulid; use validator::Validate; /// # Channel Type -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema, Default)] enum ChannelType { /// Text Channel + #[default] Text, /// Voice Channel Voice, } -impl Default for ChannelType { - fn default() -> Self { - ChannelType::Text - } -} - /// # Channel Data #[derive(Validate, Serialize, Deserialize, JsonSchema)] pub struct DataCreateChannel { diff --git a/crates/delta/src/routes/servers/server_edit.rs b/crates/delta/src/routes/servers/server_edit.rs index 267b0a2a..765c311a 100644 --- a/crates/delta/src/routes/servers/server_edit.rs +++ b/crates/delta/src/routes/servers/server_edit.rs @@ -33,6 +33,10 @@ pub struct DataEditServer { /// System message configuration system_messages: Option, + /// Bitfield of server flags + #[serde(skip_serializing_if = "Option::is_none")] + pub flags: Option, + // Whether this server is age-restricted // nsfw: Option, /// Whether this server is public and should show up on [Revolt Discover](https://rvlt.gg) @@ -92,6 +96,11 @@ pub async fn req( .await?; } + // Check we are privileged if changing sensitive fields + if data.flags.is_some() && !user.privileged { + return Err(Error::NotPrivileged); + } + if data.categories.is_some() { permissions .throw_permission(db, Permission::ManageChannel) @@ -105,6 +114,7 @@ pub async fn req( banner, categories, system_messages, + flags, // nsfw, discoverable, analytics, @@ -116,6 +126,7 @@ pub async fn req( description, categories, system_messages, + flags, // nsfw, discoverable, analytics, diff --git a/crates/delta/src/routes/users/edit_user.rs b/crates/delta/src/routes/users/edit_user.rs index 40788c96..c7c2b30c 100644 --- a/crates/delta/src/routes/users/edit_user.rs +++ b/crates/delta/src/routes/users/edit_user.rs @@ -1,6 +1,6 @@ use revolt_quark::models::user::{FieldsUser, PartialUser, User}; use revolt_quark::models::File; -use revolt_quark::{Database, Error, Result}; +use revolt_quark::{Database, Error, Ref, Result}; use revolt_quark::models::user::UserStatus; use rocket::serde::json::Json; @@ -24,6 +24,10 @@ pub struct UserProfileData { /// # User Data #[derive(Validate, Serialize, Deserialize, JsonSchema)] pub struct DataEditUser { + /// Attachment Id for avatar + #[validate(length(min = 1, max = 128))] + avatar: Option, + /// New user status #[validate] status: Option, @@ -32,9 +36,14 @@ pub struct DataEditUser { /// This is applied as a partial. #[validate] profile: Option, - /// Attachment Id for avatar - #[validate(length(min = 1, max = 128))] - avatar: Option, + + /// Bitfield of user badges + #[serde(skip_serializing_if = "Option::is_none")] + badges: Option, + /// Enum of user flags + #[serde(skip_serializing_if = "Option::is_none")] + flags: Option, + /// Fields to remove from user object #[validate(length(min = 1))] remove: Option>, @@ -44,19 +53,36 @@ pub struct DataEditUser { /// /// Edit currently authenticated user. #[openapi(tag = "User Information")] -#[patch("/@me", data = "")] +#[patch("/", data = "")] pub async fn req( db: &State, mut user: User, + target: Ref, data: Json, ) -> Result> { let data = data.into_inner(); data.validate() .map_err(|error| Error::FailedValidation { error })?; + // If we want to edit a different user than self, ensure we have + // permissions and subsequently replace the user in question + if target.id != "@me" { + if !user.privileged { + return Err(Error::NotPrivileged); + } + + user = target.as_user(db).await?; + // Otherwise, filter out invalid edit fields + } else if data.badges.is_some() || data.flags.is_some() { + return Err(Error::NotPrivileged); + } + + // Exit out early if nothing is changed if data.status.is_none() && data.profile.is_none() && data.avatar.is_none() + && data.badges.is_none() + && data.flags.is_none() && data.remove.is_none() { return Ok(Json(user)); @@ -83,7 +109,11 @@ pub async fn req( } } - let mut partial: PartialUser = Default::default(); + let mut partial: PartialUser = PartialUser { + badges: data.badges, + flags: data.flags, + ..Default::default() + }; // 2. Apply new avatar if let Some(avatar) = data.avatar { diff --git a/crates/quark/Cargo.toml b/crates/quark/Cargo.toml index 11b2ff1e..916d9b33 100644 --- a/crates/quark/Cargo.toml +++ b/crates/quark/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-quark" -version = "0.5.9" +version = "0.5.17" 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" @@ -67,8 +67,7 @@ impl_ops = "0.1.1" num_enum = "0.5.6" reqwest = "0.11.10" bitfield = "0.13.2" -once_cell = "1.13.0" -lazy_static = "1.4.0" +once_cell = "1.17.1" async-lock = "2.6.0" lru = { version = "0.7.6", optional = true } diff --git a/crates/quark/src/events/impl.rs b/crates/quark/src/events/impl.rs index d4b36c19..6010e6f0 100644 --- a/crates/quark/src/events/impl.rs +++ b/crates/quark/src/events/impl.rs @@ -557,7 +557,7 @@ impl EventV1 { /// Publish private event pub async fn private(self, id: String) { - self.p(format!("{}!", id)).await; + self.p(format!("{id}!")).await; } /// Publish internal global event diff --git a/crates/quark/src/impl/dummy/admin/stats.rs b/crates/quark/src/impl/dummy/admin/stats.rs new file mode 100644 index 00000000..a5fc808e --- /dev/null +++ b/crates/quark/src/impl/dummy/admin/stats.rs @@ -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 { + todo!() + } +} diff --git a/crates/quark/src/impl/dummy/channels/message.rs b/crates/quark/src/impl/dummy/channels/message.rs index a01a32a1..c88ce11f 100644 --- a/crates/quark/src/impl/dummy/channels/message.rs +++ b/crates/quark/src/impl/dummy/channels/message.rs @@ -1,4 +1,4 @@ -use crate::models::message::{AppendMessage, Message, MessageSort, PartialMessage}; +use crate::models::message::{AppendMessage, Message, MessageQuery, PartialMessage}; use crate::{AbstractMessage, Result}; use super::super::DummyDb; @@ -41,28 +41,8 @@ impl AbstractMessage for DummyDb { Ok(()) } - async fn fetch_messages( - &self, - channel: &str, - _limit: Option, - _before: Option, - _after: Option, - _sort: Option, - _nearby: Option, - ) -> Result> { - Ok(vec![self.fetch_message(channel).await.unwrap()]) - } - - async fn search_messages( - &self, - channel: &str, - _query: &str, - _limit: Option, - _before: Option, - _after: Option, - _sort: MessageSort, - ) -> Result> { - Ok(vec![self.fetch_message(channel).await.unwrap()]) + async fn fetch_messages(&self, _query: MessageQuery) -> Result> { + Ok(vec![]) } /// Add a new reaction to a message diff --git a/crates/quark/src/impl/dummy/mod.rs b/crates/quark/src/impl/dummy/mod.rs index 92b1f315..9d93eebd 100644 --- a/crates/quark/src/impl/dummy/mod.rs +++ b/crates/quark/src/impl/dummy/mod.rs @@ -2,6 +2,7 @@ use crate::AbstractDatabase; pub mod admin { pub mod migrations; + pub mod stats; } pub mod media { diff --git a/crates/quark/src/impl/dummy/safety/report.rs b/crates/quark/src/impl/dummy/safety/report.rs index 5286976c..3286f997 100644 --- a/crates/quark/src/impl/dummy/safety/report.rs +++ b/crates/quark/src/impl/dummy/safety/report.rs @@ -1,3 +1,4 @@ +use crate::models::report::PartialReport; use crate::models::Report; use crate::{AbstractReport, Result}; @@ -9,4 +10,16 @@ impl AbstractReport for DummyDb { info!("Insert {:?}", report); Ok(()) } + + async fn update_report(&self, _id: &str, _report: &PartialReport) -> Result<()> { + todo!() + } + + async fn fetch_report(&self, _report_id: &str) -> Result { + todo!() + } + + async fn fetch_reports(&self) -> Result> { + Ok(vec![]) + } } diff --git a/crates/quark/src/impl/dummy/safety/snapshot.rs b/crates/quark/src/impl/dummy/safety/snapshot.rs index 8d384059..a99447dd 100644 --- a/crates/quark/src/impl/dummy/safety/snapshot.rs +++ b/crates/quark/src/impl/dummy/safety/snapshot.rs @@ -9,4 +9,8 @@ impl AbstractSnapshot for DummyDb { info!("Insert {:?}", snapshot); Ok(()) } + + async fn fetch_snapshot(&self, _report_id: &str) -> Result { + todo!() + } } diff --git a/crates/quark/src/impl/generic/channels/channel_invite.rs b/crates/quark/src/impl/generic/channels/channel_invite.rs index ac9f645b..4193c34d 100644 --- a/crates/quark/src/impl/generic/channels/channel_invite.rs +++ b/crates/quark/src/impl/generic/channels/channel_invite.rs @@ -5,13 +5,11 @@ use crate::{ Database, Error, Result, }; -lazy_static! { - static ref ALPHABET: [char; 54] = [ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', - 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', - 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' - ]; -} +static ALPHABET: [char; 54] = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' +]; impl Invite { /// Get the invite code for this invite @@ -30,7 +28,7 @@ impl Invite { /// Create a new invite from given information pub async fn create(db: &Database, creator: &User, target: &Channel) -> Result { - let code = nanoid!(8, &*ALPHABET); + let code = nanoid!(8, &ALPHABET); let invite = match &target { Channel::Group { id, .. } => Ok(Invite::Group { code, diff --git a/crates/quark/src/impl/generic/channels/message.rs b/crates/quark/src/impl/generic/channels/message.rs index 2e66ea45..65715c22 100644 --- a/crates/quark/src/impl/generic/channels/message.rs +++ b/crates/quark/src/impl/generic/channels/message.rs @@ -385,7 +385,7 @@ impl SendableEmbed { impl BulkMessageResponse { pub async fn transform( db: &Database, - channel: &Channel, + channel: Option<&Channel>, messages: Vec, include_users: Option, ) -> Result { @@ -394,7 +394,8 @@ impl BulkMessageResponse { let users = User::fetch_foreign_users(db, &user_ids).await?; Ok(match channel { - Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { + Some(Channel::TextChannel { server, .. }) + | Some(Channel::VoiceChannel { server, .. }) => { BulkMessageResponse::MessagesAndUsers { messages, users, diff --git a/crates/quark/src/impl/generic/media/emoji.rs b/crates/quark/src/impl/generic/media/emoji.rs index 6c58b38a..77b3677f 100644 --- a/crates/quark/src/impl/generic/media/emoji.rs +++ b/crates/quark/src/impl/generic/media/emoji.rs @@ -1,4 +1,5 @@ use std::{collections::HashSet, str::FromStr}; +use once_cell::sync::Lazy; use ulid::Ulid; @@ -8,13 +9,10 @@ use crate::{ Database, Result, }; -lazy_static! { - /// Permissible emojis - static ref PERMISSIBLE_EMOJIS: HashSet = include_str!(crate::asset!("emojis.txt")) - .split('\n') - .map(|x| x.into()) - .collect(); -} +static PERMISSIBLE_EMOJIS: Lazy> = Lazy::new(|| include_str!(crate::asset!("emojis.txt")) + .split('\n') + .map(|x| x.into()) + .collect()); impl Emoji { /// Get parent id diff --git a/crates/quark/src/impl/generic/mod.rs b/crates/quark/src/impl/generic/mod.rs index 5cf905df..08bcd03c 100644 --- a/crates/quark/src/impl/generic/mod.rs +++ b/crates/quark/src/impl/generic/mod.rs @@ -28,3 +28,7 @@ pub mod users { pub mod user; pub mod user_settings; } + +pub mod safety { + pub mod report; +} diff --git a/crates/quark/src/impl/generic/safety/report.rs b/crates/quark/src/impl/generic/safety/report.rs new file mode 100644 index 00000000..259d7701 --- /dev/null +++ b/crates/quark/src/impl/generic/safety/report.rs @@ -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 + } +} diff --git a/crates/quark/src/impl/mongo/admin/migrations/init.rs b/crates/quark/src/impl/mongo/admin/migrations/init.rs index 874e0d92..4501381c 100644 --- a/crates/quark/src/impl/mongo/admin/migrations/init.rs +++ b/crates/quark/src/impl/mongo/admin/migrations/init.rs @@ -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" } ] }, diff --git a/crates/quark/src/impl/mongo/admin/migrations/scripts.rs b/crates/quark/src/impl/mongo/admin/migrations/scripts.rs index 5fbd4b9c..168029e6 100644 --- a/crates/quark/src/impl/mongo/admin/migrations/scripts.rs +++ b/crates/quark/src/impl/mongo/admin/migrations/scripts.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{time::Duration, ops::BitXor}; use bson::{Bson, DateTime}; use futures::StreamExt; @@ -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::("migrations"); @@ -503,13 +503,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 { update.insert( "default_permissions", - (*DEFAULT_PERMISSION_SERVER - // Remove Send Message permission if it wasn't originally granted - ^ (if has_send { - 0 - } else { - Permission::SendMessage as u64 - })) as i64, + // Remove Send Message permission if it wasn't originally granted + DEFAULT_PERMISSION_SERVER.bitxor(if has_send { 0 } else { Permission::SendMessage as u64}) as i64, ); if let Some(Bson::Document(mut roles)) = document.remove("roles") { @@ -661,6 +656,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::("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. diff --git a/crates/quark/src/impl/mongo/admin/stats.rs b/crates/quark/src/impl/mongo/admin/stats.rs new file mode 100644 index 00000000..8fa1e2b8 --- /dev/null +++ b/crates/quark/src/impl/mongo/admin/stats.rs @@ -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 { + 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::(&collection) + .aggregate( + vec![doc! { + "$indexStats": { } + }], + None, + ) + .await + .map_err(|_| Error::DatabaseError { + operation: "aggregate", + with: "col", + })? + .filter_map(|s| async { s.ok() }) + .collect::>() + .await + .into_iter() + .filter_map(|doc| from_document(doc).ok()) + .collect::>(), + ); + + coll_stats.insert( + collection.to_string(), + self.col::(&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::>() + .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, + }) + } +} diff --git a/crates/quark/src/impl/mongo/channels/message.rs b/crates/quark/src/impl/mongo/channels/message.rs index 0a1db531..79ae1916 100644 --- a/crates/quark/src/impl/mongo/channels/message.rs +++ b/crates/quark/src/impl/mongo/channels/message.rs @@ -2,7 +2,9 @@ use bson::{to_bson, Document}; use futures::try_join; use mongodb::options::FindOptions; -use crate::models::message::{AppendMessage, Message, MessageSort, PartialMessage}; +use crate::models::message::{ + AppendMessage, Message, MessageQuery, MessageSort, MessageTimePeriod, PartialMessage, +}; use crate::r#impl::mongo::DocumentId; use crate::{AbstractMessage, Error, Result}; @@ -139,145 +141,138 @@ impl AbstractMessage for MongoDb { .await } - async fn fetch_messages( - &self, - channel: &str, - limit: Option, - before: Option, - after: Option, - sort: Option, - nearby: Option, - ) -> Result> { - let limit = limit.unwrap_or(50); - Ok(if let Some(nearby) = nearby { - let (a, b) = try_join!( - self.find_with_options::<_, Message>( - COL, + async fn fetch_messages(&self, query: MessageQuery) -> Result> { + let mut filter = doc! {}; + + // 1. Apply message filters + if let Some(channel) = query.filter.channel { + filter.insert("channel", channel); + } + + if let Some(author) = query.filter.author { + filter.insert("author", author); + } + + let is_search_query = if let Some(query) = query.filter.query { + filter.insert( + "$text", + doc! { + "$search": query + }, + ); + + true + } else { + false + }; + + // 2. Find query limit + let limit = query.limit.unwrap_or(50); + + // 3. Apply message time period + match query.time_period { + MessageTimePeriod::Relative { nearby } => { + // 3.1. Prepare filters + let mut older_message_filter = filter.clone(); + let mut newer_message_filter = filter; + + older_message_filter.insert( + "_id", doc! { - "channel": channel, - "_id": { - "$gte": &nearby - } + "$lt": &nearby }, - FindOptions::builder() - .limit(limit / 2 + 1) - .sort(doc! { - "_id": 1_i32 - }) - .build(), - ), - self.find_with_options::<_, Message>( - COL, + ); + + newer_message_filter.insert( + "_id", doc! { - "channel": channel, - "_id": { - "$lt": &nearby - } + "$gte": &nearby }, + ); + + // 3.2. Execute in both directions + let (a, b) = try_join!( + self.find_with_options::<_, Message>( + COL, + newer_message_filter, + FindOptions::builder() + .limit(limit / 2 + 1) + .sort(doc! { + "_id": 1_i32 + }) + .build(), + ), + self.find_with_options::<_, Message>( + COL, + older_message_filter, + FindOptions::builder() + .limit(limit / 2) + .sort(doc! { + "_id": -1_i32 + }) + .build(), + ) + )?; + + Ok([a, b].concat()) + } + MessageTimePeriod::Absolute { + before, + after, + sort, + } => { + // 3.1. Apply message ID filter + if let Some(doc) = match (before, after) { + (Some(before), Some(after)) => Some(doc! { + "$lt": before, + "$gt": after + }), + (Some(before), _) => Some(doc! { + "$lt": before + }), + (_, Some(after)) => Some(doc! { + "$gt": after + }), + _ => None, + } { + filter.insert("_id", doc); + } + + // 3.2. Execute with given message sort + self.find_with_options( + COL, + filter, FindOptions::builder() - .limit(limit / 2) - .sort(doc! { - "_id": -1_i32 + .limit(limit) + .sort(match sort.unwrap_or(MessageSort::Latest) { + // Sort by relevance, fallback to latest + MessageSort::Relevance => { + if is_search_query { + doc! { + "score": { + "$meta": "textScore" + } + } + } else { + doc! { + "_id": -1_i32 + } + } + } + // Sort by latest first + MessageSort::Latest => doc! { + "_id": -1_i32 + }, + // Sort by oldest first + MessageSort::Oldest => doc! { + "_id": 1_i32 + }, }) .build(), ) - )?; - - [a, b].concat() - } else { - let mut query = doc! { "channel": channel }; - if let Some(before) = before { - query.insert("_id", doc! { "$lt": before }); + .await } - - if let Some(after) = after { - query.insert("_id", doc! { "$gt": after }); - } - - let sort: i32 = if let MessageSort::Latest = sort.unwrap_or(MessageSort::Latest) { - -1 - } else { - 1 - }; - - self.find_with_options::<_, Message>( - COL, - query, - FindOptions::builder() - .limit(limit) - .sort(doc! { - "_id": sort - }) - .build(), - ) - .await? - }) - } - - async fn search_messages( - &self, - channel: &str, - query: &str, - limit: Option, - before: Option, - after: Option, - sort: MessageSort, - ) -> Result> { - let limit = limit.unwrap_or(50); - - let mut filter = doc! { - "channel": channel, - "$text": { - "$search": query - } - }; - - if let Some(doc) = match (before, after) { - (Some(before), Some(after)) => Some(doc! { - "lt": before, - "gt": after - }), - (Some(before), _) => Some(doc! { - "lt": before - }), - (_, Some(after)) => Some(doc! { - "gt": after - }), - _ => None, - } { - filter.insert("_id", doc); } - - self.find_with_options( - COL, - filter, - FindOptions::builder() - .projection(if let MessageSort::Relevance = &sort { - doc! { - "score": { - "$meta": "textScore" - } - } - } else { - doc! {} - }) - .limit(limit) - .sort(match &sort { - MessageSort::Relevance => doc! { - "score": { - "$meta": "textScore" - } - }, - MessageSort::Latest => doc! { - "_id": -1_i32 - }, - MessageSort::Oldest => doc! { - "_id": 1_i32 - }, - }) - .build(), - ) - .await } /// Add a new reaction to a message diff --git a/crates/quark/src/impl/mongo/media/attachment.rs b/crates/quark/src/impl/mongo/media/attachment.rs index caeae66a..204ca712 100644 --- a/crates/quark/src/impl/mongo/media/attachment.rs +++ b/crates/quark/src/impl/mongo/media/attachment.rs @@ -37,7 +37,7 @@ impl AbstractAttachment for MongoDb { parent_type: &str, parent_id: &str, ) -> Result { - let key = format!("{}_id", parent_type); + let key = format!("{parent_type}_id"); match self .find_one::( COL, diff --git a/crates/quark/src/impl/mongo/mod.rs b/crates/quark/src/impl/mongo/mod.rs index 2efc84f8..5c1b772a 100644 --- a/crates/quark/src/impl/mongo/mod.rs +++ b/crates/quark/src/impl/mongo/mod.rs @@ -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 { @@ -90,17 +91,25 @@ impl MongoDb { where O: Into>, { - Ok(self - .col::(collection) - .find(projection, options) - .await - .map_err(|_| Error::DatabaseError { + let result = self.col::(collection).find(projection, options).await; + Ok(if cfg!(debug_assertions) { + result.unwrap() + } else { + result.map_err(|_| Error::DatabaseError { operation: "find", with: collection, })? - .filter_map(|s| async { s.ok() }) - .collect::>() - .await) + } + .filter_map(|s| async { + if cfg!(debug_assertions) { + // Hard fail on invalid documents + Some(s.unwrap()) + } else { + s.ok() + } + }) + .collect::>() + .await) } async fn find( diff --git a/crates/quark/src/impl/mongo/safety/report.rs b/crates/quark/src/impl/mongo/safety/report.rs index 6b364d11..69888e88 100644 --- a/crates/quark/src/impl/mongo/safety/report.rs +++ b/crates/quark/src/impl/mongo/safety/report.rs @@ -1,3 +1,4 @@ +use crate::models::report::PartialReport; use crate::models::Report; use crate::{AbstractReport, Result}; @@ -10,4 +11,18 @@ impl AbstractReport for MongoDb { async fn insert_report(&self, report: &Report) -> Result<()> { 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 { + self.find_one_by_id(COL, report_id).await + } + + async fn fetch_reports(&self) -> Result> { + self.find(COL, doc! {}).await + } } diff --git a/crates/quark/src/impl/mongo/safety/snapshot.rs b/crates/quark/src/impl/mongo/safety/snapshot.rs index d4dd29f6..6b31de9f 100644 --- a/crates/quark/src/impl/mongo/safety/snapshot.rs +++ b/crates/quark/src/impl/mongo/safety/snapshot.rs @@ -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 { + self.find_one( + COL, + doc! { + "report_id": report_id + }, + ) + .await + } } diff --git a/crates/quark/src/impl/mongo/users/user.rs b/crates/quark/src/impl/mongo/users/user.rs index a5459f9d..bbfe84f1 100644 --- a/crates/quark/src/impl/mongo/users/user.rs +++ b/crates/quark/src/impl/mongo/users/user.rs @@ -1,6 +1,7 @@ use bson::Document; use futures::StreamExt; use mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions}; +use once_cell::sync::Lazy; use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus, User}; use crate::r#impl::mongo::IntoDocumentPath; @@ -8,16 +9,14 @@ use crate::{AbstractUser, Error, Result}; use super::super::MongoDb; -lazy_static! { - static ref FIND_USERNAME_OPTIONS: FindOneOptions = FindOneOptions::builder() - .collation( - Collation::builder() - .locale("en") - .strength(CollationStrength::Secondary) - .build() - ) - .build(); -} +static FIND_USERNAME_OPTIONS: Lazy = Lazy::new(|| FindOneOptions::builder() + .collation( + Collation::builder() + .locale("en") + .strength(CollationStrength::Secondary) + .build() + ) + .build()); static COL: &str = "users"; @@ -265,7 +264,7 @@ impl AbstractUser for MongoDb { [ { "_id": target_id, - "status": format!("{:?}", relationship) + "status": format!("{relationship:?}") } ] ] diff --git a/crates/quark/src/lib.rs b/crates/quark/src/lib.rs index 2d063639..13b1ce82 100644 --- a/crates/quark/src/lib.rs +++ b/crates/quark/src/lib.rs @@ -11,8 +11,6 @@ extern crate impl_ops; #[macro_use] extern crate optional_struct; #[macro_use] -extern crate lazy_static; -#[macro_use] extern crate bitfield; #[macro_use] extern crate bson; diff --git a/crates/quark/src/models/admin/stats.rs b/crates/quark/src/models/admin/stats.rs new file mode 100644 index 00000000..7866cd8c --- /dev/null +++ b/crates/quark/src/models/admin/stats.rs @@ -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, +} + +/// 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, + + /// 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, + + /// 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>, + + /// Collection stats + pub coll_stats: HashMap, +} diff --git a/crates/quark/src/models/channels/message.rs b/crates/quark/src/models/channels/message.rs index 12c20deb..f8912e73 100644 --- a/crates/quark/src/models/channels/message.rs +++ b/crates/quark/src/models/channels/message.rs @@ -160,10 +160,11 @@ pub struct Message { /// # Message Sort /// /// Sort used for retrieving messages -#[derive(Serialize, Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema, Debug, Default)] #[cfg_attr(feature = "rocket_impl", derive(FromFormField))] pub enum MessageSort { /// Sort by the most relevant messages + #[default] Relevance, /// Sort by the newest messages first Latest, @@ -171,10 +172,54 @@ pub enum MessageSort { Oldest, } -impl Default for MessageSort { - fn default() -> MessageSort { - MessageSort::Relevance - } +/// # Message Time Period +/// +/// Filter and sort messages by time +#[derive(Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum MessageTimePeriod { + Relative { + /// Message id to search around + /// + /// Specifying 'nearby' ignores 'before', 'after' and 'sort'. + /// It will also take half of limit rounded as the limits to each side. + /// It also fetches the message ID specified. + nearby: String, + }, + Absolute { + /// Message id before which messages should be fetched + before: Option, + /// Message id after which messages should be fetched + after: Option, + /// Message sort direction + sort: Option, + }, +} + +/// # Message Filter +#[derive(Serialize, Deserialize, JsonSchema, Default)] +pub struct MessageFilter { + /// Parent channel ID + pub channel: Option, + /// Message author ID + pub author: Option, + /// Search query + pub query: Option, +} + +/// # Message Query +#[derive(Serialize, Deserialize, JsonSchema)] +pub struct MessageQuery { + /// Maximum number of messages to fetch + /// + /// For fetching nearby messages, this is \`(limit + 1)\`. + pub limit: Option, + /// Filter to apply + #[serde(flatten)] + pub filter: MessageFilter, + /// Time period to fetch + #[serde(flatten)] + pub time_period: MessageTimePeriod, } /// # Bulk Message Response diff --git a/crates/quark/src/models/media/attachment.rs b/crates/quark/src/models/media/attachment.rs index 120da29a..4cef5dfb 100644 --- a/crates/quark/src/models/media/attachment.rs +++ b/crates/quark/src/models/media/attachment.rs @@ -1,10 +1,11 @@ use serde::{Deserialize, Serialize}; /// Metadata associated with file -#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)] #[serde(tag = "type")] pub enum Metadata { /// File is just a generic uncategorised file + #[default] File, /// File contains textual data and should be displayed as such Text, @@ -16,12 +17,6 @@ pub enum Metadata { Audio, } -impl Default for Metadata { - fn default() -> Metadata { - Metadata::File - } -} - /// Representation of a File on Revolt /// Generated by Autumn #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)] diff --git a/crates/quark/src/models/mod.rs b/crates/quark/src/models/mod.rs index 1a3573d9..ff5681ba 100644 --- a/crates/quark/src/models/mod.rs +++ b/crates/quark/src/models/mod.rs @@ -1,6 +1,7 @@ mod admin { pub mod migrations; pub mod simple; + pub mod stats; } mod media { diff --git a/crates/quark/src/models/safety/report.rs b/crates/quark/src/models/safety/report.rs index db4552e8..f9ee2a02 100644 --- a/crates/quark/src/models/safety/report.rs +++ b/crates/quark/src/models/safety/report.rs @@ -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,8 +71,25 @@ pub enum ReportedContent { }, } -/// User-generated platform moderation report. +/// 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, 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")] @@ -82,4 +100,11 @@ pub struct Report { pub content: ReportedContent, /// 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, } diff --git a/crates/quark/src/models/safety/snapshot.rs b/crates/quark/src/models/safety/snapshot.rs index f9674de8..90eb7687 100644 --- a/crates/quark/src/models/safety/snapshot.rs +++ b/crates/quark/src/models/safety/snapshot.rs @@ -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, /// Context after the message - #[serde(rename = "_leading_context")] + #[serde(rename = "_leading_context", default)] leading_context: Vec, /// 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, + /// Channels involved in snapshot + #[serde(rename = "_channels", skip_serializing_if = "Vec::is_empty")] + pub channels: Vec, + /// Server involved in snapshot + #[serde(rename = "_server", skip_serializing_if = "Option::is_none")] + pub server: Option, +} diff --git a/crates/quark/src/models/servers/server.rs b/crates/quark/src/models/servers/server.rs index 0df877b3..375b1447 100644 --- a/crates/quark/src/models/servers/server.rs +++ b/crates/quark/src/models/servers/server.rs @@ -118,7 +118,7 @@ pub struct Server { #[serde(skip_serializing_if = "Option::is_none")] pub banner: Option, - /// Enum of server flags + /// Bitfield of server flags #[serde(skip_serializing_if = "Option::is_none")] pub flags: Option, diff --git a/crates/quark/src/permissions/defn/permission.rs b/crates/quark/src/permissions/defn/permission.rs index 2ebb0257..a73c6216 100644 --- a/crates/quark/src/permissions/defn/permission.rs +++ b/crates/quark/src/permissions/defn/permission.rs @@ -1,6 +1,7 @@ use num_enum::TryFromPrimitive; use serde::{Deserialize, Serialize}; -use std::ops; +use once_cell::sync::Lazy; +use std::ops::{self, Add}; /// Permission value on Revolt /// @@ -97,24 +98,18 @@ pub enum Permission { impl_op_ex!(+ |a: &Permission, b: &Permission| -> u64 { *a as u64 | *b as u64 }); impl_op_ex_commutative!(+ |a: &u64, b: &Permission| -> u64 { *a | *b as u64 }); -lazy_static! { - pub static ref ALLOW_IN_TIMEOUT: u64 = Permission::ViewChannel + Permission::ReadMessageHistory; - pub static ref DEFAULT_PERMISSION_VIEW_ONLY: u64 = - Permission::ViewChannel + Permission::ReadMessageHistory; - pub static ref DEFAULT_PERMISSION: u64 = *DEFAULT_PERMISSION_VIEW_ONLY - + Permission::SendMessage - + Permission::InviteOthers - + Permission::SendEmbeds - + Permission::UploadFiles - + Permission::Connect - + Permission::Speak; - pub static ref DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = Permission::GrantAllSafe as u64; - pub static ref DEFAULT_PERMISSION_DIRECT_MESSAGE: u64 = *DEFAULT_PERMISSION - + Permission::ManageChannel - + Permission::React; - pub static ref DEFAULT_PERMISSION_SERVER: u64 = - *DEFAULT_PERMISSION + Permission::React + Permission::ChangeNickname + Permission::ChangeAvatar; -} +pub static ALLOW_IN_TIMEOUT: Lazy = Lazy::new(|| Permission::ViewChannel + Permission::ReadMessageHistory); +pub static DEFAULT_PERMISSION_VIEW_ONLY: Lazy = Lazy::new(|| Permission::ViewChannel + Permission::ReadMessageHistory); +pub static DEFAULT_PERMISSION: Lazy = Lazy::new(|| DEFAULT_PERMISSION_VIEW_ONLY.add( + Permission::SendMessage + + Permission::InviteOthers + + Permission::SendEmbeds + + Permission::UploadFiles + + Permission::Connect + + Permission::Speak)); +pub static DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = Permission::GrantAllSafe as u64; +pub static DEFAULT_PERMISSION_DIRECT_MESSAGE: Lazy = Lazy::new(|| DEFAULT_PERMISSION.add(Permission::ManageChannel + Permission::React)); +pub static DEFAULT_PERMISSION_SERVER: Lazy = Lazy::new(|| DEFAULT_PERMISSION.add(Permission::React + Permission::ChangeNickname + Permission::ChangeAvatar)); bitfield! { #[derive(Default)] diff --git a/crates/quark/src/permissions/impl/permission.rs b/crates/quark/src/permissions/impl/permission.rs index 04ed720f..7ff36616 100644 --- a/crates/quark/src/permissions/impl/permission.rs +++ b/crates/quark/src/permissions/impl/permission.rs @@ -113,7 +113,7 @@ async fn calculate_channel_permission( let value: PermissionValue = match channel { Channel::SavedMessages { user, .. } => { if user == &data.perspective.id { - (*DEFAULT_PERMISSION_SAVED_MESSAGES).into() + DEFAULT_PERMISSION_SAVED_MESSAGES.into() } else { 0_u64.into() } diff --git a/crates/quark/src/permissions/impl/user.rs b/crates/quark/src/permissions/impl/user.rs index 22c1939e..c1ed1054 100644 --- a/crates/quark/src/permissions/impl/user.rs +++ b/crates/quark/src/permissions/impl/user.rs @@ -51,6 +51,10 @@ pub fn get_relationship(a: &User, b: &str) -> RelationshipStatus { async fn calculate_permission(data: &mut PermissionCalculator<'_>, db: &crate::Database) -> u32 { let user = data.user.get().unwrap(); + if data.perspective.privileged { + return u32::MAX; + } + if data.perspective.id == user.id { return u32::MAX; } diff --git a/crates/quark/src/presence/entry.rs b/crates/quark/src/presence/entry.rs index 8373ae25..4d43cd2e 100644 --- a/crates/quark/src/presence/entry.rs +++ b/crates/quark/src/presence/entry.rs @@ -1,14 +1,14 @@ use std::env; use serde::{Deserialize, Serialize}; +use once_cell::sync::Lazy; -lazy_static! { - pub static ref REGION_ID: u16 = env::var("REGION_ID") - .unwrap_or_else(|_| "0".to_string()) - .parse() - .unwrap(); - pub static ref REGION_KEY: String = format!("region{}", &*REGION_ID); -} +pub static REGION_ID: Lazy = Lazy::new(|| env::var("REGION_ID") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap()); + +pub static REGION_KEY: Lazy = Lazy::new(|| format!("region{}", &*REGION_ID)); /// Compact presence information for a user #[derive(Serialize, Deserialize, Debug)] diff --git a/crates/quark/src/tasks/ack.rs b/crates/quark/src/tasks/ack.rs index eee72be1..25cf6e2b 100644 --- a/crates/quark/src/tasks/ack.rs +++ b/crates/quark/src/tasks/ack.rs @@ -2,8 +2,8 @@ use crate::Database; use deadqueue::limited::Queue; -use mongodb::bson::doc; use std::{collections::HashMap, time::Duration}; +use once_cell::sync::Lazy; use super::DelayedTask; @@ -38,9 +38,7 @@ struct Task { event: AckEvent, } -lazy_static! { - static ref Q: Queue = Queue::new(10_000); -} +static Q: Lazy> = Lazy::new(|| Queue::new(10_000)); /// Queue a new task for a worker pub async fn queue(channel: String, user: String, event: AckEvent) { diff --git a/crates/quark/src/tasks/last_message_id.rs b/crates/quark/src/tasks/last_message_id.rs index a6264578..976a725d 100644 --- a/crates/quark/src/tasks/last_message_id.rs +++ b/crates/quark/src/tasks/last_message_id.rs @@ -2,8 +2,8 @@ use crate::{models::channel::PartialChannel, Database}; use deadqueue::limited::Queue; -use mongodb::bson::doc; use std::{collections::HashMap, time::Duration}; +use once_cell::sync::Lazy; use super::DelayedTask; @@ -26,9 +26,7 @@ struct Task { is_dm: bool, } -lazy_static! { - static ref Q: Queue = Queue::new(10_000); -} +static Q: Lazy> = Lazy::new(|| Queue::new(10_000)); /// Queue a new task for a worker pub async fn queue(channel: String, id: String, is_dm: bool) { diff --git a/crates/quark/src/tasks/process_embeds.rs b/crates/quark/src/tasks/process_embeds.rs index d36d0496..aef165fb 100644 --- a/crates/quark/src/tasks/process_embeds.rs +++ b/crates/quark/src/tasks/process_embeds.rs @@ -9,6 +9,7 @@ use async_lock::Semaphore; use async_std::task::spawn; use deadqueue::limited::Queue; use std::sync::Arc; +use once_cell::sync::Lazy; /// Task information #[derive(Debug)] @@ -21,9 +22,8 @@ struct EmbedTask { content: String, } -lazy_static! { - static ref Q: Queue = Queue::new(10_000); -} +static Q: Lazy> = Lazy::new(|| Queue::new(10_000)); + /// Queue a new task for a worker pub async fn queue(channel: String, id: String, content: String) { diff --git a/crates/quark/src/tasks/web_push.rs b/crates/quark/src/tasks/web_push.rs index c9428f6f..b0c5acb1 100644 --- a/crates/quark/src/tasks/web_push.rs +++ b/crates/quark/src/tasks/web_push.rs @@ -1,8 +1,8 @@ -use crate::bson::doc; use crate::util::variables::delta::VAPID_PRIVATE_KEY; use authifier::Database; use deadqueue::limited::Queue; +use once_cell::sync::Lazy; use web_push::{ ContentEncoding, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder, @@ -17,9 +17,8 @@ struct PushTask { payload: String, } -lazy_static! { - static ref Q: Queue = Queue::new(10_000); -} +static Q: Lazy> = Lazy::new(|| Queue::new(10_000)); + /// Queue a new task for a worker pub async fn queue(recipients: Vec, payload: String) { diff --git a/crates/quark/src/traits/admin/stats.rs b/crates/quark/src/traits/admin/stats.rs new file mode 100644 index 00000000..99e10819 --- /dev/null +++ b/crates/quark/src/traits/admin/stats.rs @@ -0,0 +1,6 @@ +use crate::{models::stats::Stats, Result}; + +#[async_trait] +pub trait AbstractStats: Sync + Send { + async fn generate_stats(&self) -> Result; +} diff --git a/crates/quark/src/traits/channels/message.rs b/crates/quark/src/traits/channels/message.rs index 6f3f4f96..c408de73 100644 --- a/crates/quark/src/traits/channels/message.rs +++ b/crates/quark/src/traits/channels/message.rs @@ -1,4 +1,4 @@ -use crate::models::message::{AppendMessage, Message, MessageSort, PartialMessage}; +use crate::models::message::{AppendMessage, Message, MessageQuery, PartialMessage}; use crate::Result; #[async_trait] @@ -21,27 +21,8 @@ pub trait AbstractMessage: Sync + Send { /// Delete messages from a channel by their ids and corresponding channel id async fn delete_messages(&self, channel: &str, ids: Vec) -> Result<()>; - /// Fetch multiple messages - async fn fetch_messages( - &self, - channel: &str, - limit: Option, - before: Option, - after: Option, - sort: Option, - nearby: Option, - ) -> Result>; - - /// Search for messages - async fn search_messages( - &self, - channel: &str, - query: &str, - limit: Option, - before: Option, - after: Option, - sort: MessageSort, - ) -> Result>; + /// Fetch multiple messages by given query + async fn fetch_messages(&self, query: MessageQuery) -> Result>; /// Add a new reaction to a message async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>; diff --git a/crates/quark/src/traits/mod.rs b/crates/quark/src/traits/mod.rs index 8671ffad..c17b8269 100644 --- a/crates/quark/src/traits/mod.rs +++ b/crates/quark/src/traits/mod.rs @@ -1,5 +1,6 @@ mod admin { pub mod migrations; + pub mod stats; } mod media { @@ -33,6 +34,7 @@ mod safety { } pub use admin::migrations::AbstractMigrations; +pub use admin::stats::AbstractStats; pub use media::attachment::AbstractAttachment; pub use media::emoji::AbstractEmoji; @@ -58,6 +60,7 @@ pub trait AbstractDatabase: Sync + Send + AbstractMigrations + + AbstractStats + AbstractAttachment + AbstractEmoji + AbstractChannel diff --git a/crates/quark/src/traits/safety/report.rs b/crates/quark/src/traits/safety/report.rs index f5a15e39..34752613 100644 --- a/crates/quark/src/traits/safety/report.rs +++ b/crates/quark/src/traits/safety/report.rs @@ -1,3 +1,4 @@ +use crate::models::report::PartialReport; use crate::models::Report; use crate::Result; @@ -5,4 +6,13 @@ use crate::Result; 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; + + /// Fetch reports + async fn fetch_reports(&self) -> Result>; } diff --git a/crates/quark/src/traits/safety/snapshot.rs b/crates/quark/src/traits/safety/snapshot.rs index 9f6ac84e..2c1e4d9c 100644 --- a/crates/quark/src/traits/safety/snapshot.rs +++ b/crates/quark/src/traits/safety/snapshot.rs @@ -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; } diff --git a/crates/quark/src/types/january.rs b/crates/quark/src/types/january.rs index 2a8859f5..e7ed95a0 100644 --- a/crates/quark/src/types/january.rs +++ b/crates/quark/src/types/january.rs @@ -5,6 +5,7 @@ use linkify::{LinkFinder, LinkKind}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::{collections::HashSet, sync::Arc}; +use once_cell::sync::Lazy; use crate::{models::attachment::File, Error, Result}; @@ -175,6 +176,9 @@ pub enum Embed { None, } +static RE_CODE: Lazy = Lazy::new(|| Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap()); +static RE_IGNORED: Lazy = Lazy::new(|| Regex::new("()").unwrap()); + impl Embed { /// Generate embeds from given content pub async fn generate( @@ -183,10 +187,7 @@ impl Embed { max_embeds: usize, semaphore: Arc, ) -> Result> { - lazy_static! { - static ref RE_CODE: Regex = Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap(); - static ref RE_IGNORED: Regex = Regex::new("()").unwrap(); - } + // Ignore code blocks. let content = RE_CODE.replace_all(&content, ""); diff --git a/crates/quark/src/util/ref.rs b/crates/quark/src/util/ref.rs index b75de9f2..5e2d863e 100644 --- a/crates/quark/src/util/ref.rs +++ b/crates/quark/src/util/ref.rs @@ -83,6 +83,11 @@ impl Ref { pub async fn as_webhook(&self, db: &Database) -> Result { db.fetch_webhook(&self.id).await } + + /// Fetch report from Ref + pub async fn as_report(&self, db: &Database) -> Result { + db.fetch_report(&self.id).await + } } impl<'r> FromParam<'r> for Ref { diff --git a/crates/quark/src/util/result.rs b/crates/quark/src/util/result.rs index 6efbd01f..66613527 100644 --- a/crates/quark/src/util/result.rs +++ b/crates/quark/src/util/result.rs @@ -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, diff --git a/crates/quark/src/util/variables/delta.rs b/crates/quark/src/util/variables/delta.rs index bb789987..3981b77d 100644 --- a/crates/quark/src/util/variables/delta.rs +++ b/crates/quark/src/util/variables/delta.rs @@ -1,73 +1,50 @@ use std::env; +use once_cell::sync::Lazy; -lazy_static! { - // Application Settings - pub static ref PUBLIC_URL: String = - env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable."); - pub static ref APP_URL: String = - env::var("REVOLT_APP_URL").expect("Missing REVOLT_APP_URL environment variable."); - pub static ref EXTERNAL_WS_URL: String = - env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable."); +// Application Settings +pub static PUBLIC_URL: Lazy = Lazy::new(|| env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable.")); +pub static APP_URL: Lazy = Lazy::new(|| env::var("REVOLT_APP_URL").expect("Missing REVOLT_APP_URL environment variable.")); +pub static EXTERNAL_WS_URL: Lazy = Lazy::new(|| env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable.")); - pub static ref AUTUMN_URL: String = - env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()); - pub static ref JANUARY_URL: String = - env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()); - pub static ref JANUARY_CONCURRENT_CONNECTIONS: usize = - env::var("JANUARY_CONCURRENT_CONNECTIONS").map_or(50, |v| v.parse().unwrap()); - pub static ref VOSO_URL: String = - env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()); - pub static ref VOSO_WS_HOST: String = - env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string()); - pub static ref VOSO_MANAGE_TOKEN: String = - env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string()); +pub static AUTUMN_URL: Lazy = Lazy::new(|| env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string())); +pub static JANUARY_URL: Lazy = Lazy::new(|| env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string())); +pub static JANUARY_CONCURRENT_CONNECTIONS: Lazy = Lazy::new(|| env::var("JANUARY_CONCURRENT_CONNECTIONS").map_or(50, |v| v.parse().unwrap())); +pub static VOSO_URL: Lazy = Lazy::new(|| env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string())); +pub static VOSO_WS_HOST: Lazy = Lazy::new(|| env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string())); +pub static VOSO_MANAGE_TOKEN: Lazy = Lazy::new(|| env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string())); - pub static ref HCAPTCHA_KEY: String = - env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string()); - pub static ref HCAPTCHA_SITEKEY: String = - env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string()); - pub static ref VAPID_PRIVATE_KEY: String = - env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable."); - pub static ref VAPID_PUBLIC_KEY: String = - env::var("REVOLT_VAPID_PUBLIC_KEY").expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable."); - pub static ref AUTHIFIER_SHIELD_KEY: Option = - env::var("REVOLT_AUTHIFIER_SHIELD_KEY").ok(); +pub static HCAPTCHA_KEY: Lazy = Lazy::new(|| env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string())); +pub static HCAPTCHA_SITEKEY: Lazy = Lazy::new(|| env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string())); +pub static VAPID_PRIVATE_KEY: Lazy = Lazy::new(|| env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable.")); +pub static VAPID_PUBLIC_KEY: Lazy = Lazy::new(|| env::var("REVOLT_VAPID_PUBLIC_KEY").expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable.")); +pub static AUTHIFIER_SHIELD_KEY: Lazy> = Lazy::new(|| env::var("REVOLT_AUTHIFIER_SHIELD_KEY").ok()); - // Application Flags - pub static ref INVITE_ONLY: bool = env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1"); - pub static ref USE_EMAIL: bool = env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or( - env::var("REVOLT_SMTP_HOST").is_ok() - && env::var("REVOLT_SMTP_USERNAME").is_ok() - && env::var("REVOLT_SMTP_PASSWORD").is_ok() - && env::var("REVOLT_SMTP_FROM").is_ok(), - |v| v == *"1" - ); - pub static ref USE_HCAPTCHA: bool = env::var("REVOLT_HCAPTCHA_KEY").is_ok(); - pub static ref USE_AUTUMN: bool = env::var("AUTUMN_PUBLIC_URL").is_ok(); - pub static ref USE_JANUARY: bool = env::var("JANUARY_PUBLIC_URL").is_ok(); - pub static ref USE_VOSO: bool = env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok(); +// Application Flags +pub static INVITE_ONLY: Lazy = Lazy::new(|| env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1")); +pub static USE_EMAIL: Lazy = Lazy::new(|| env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or( + env::var("REVOLT_SMTP_HOST").is_ok() + && env::var("REVOLT_SMTP_USERNAME").is_ok() + && env::var("REVOLT_SMTP_PASSWORD").is_ok() + && env::var("REVOLT_SMTP_FROM").is_ok(), + |v| v == *"1" +)); +pub static USE_HCAPTCHA: Lazy = Lazy::new(|| env::var("REVOLT_HCAPTCHA_KEY").is_ok()); +pub static USE_AUTUMN: Lazy = Lazy::new(|| env::var("AUTUMN_PUBLIC_URL").is_ok()); +pub static USE_JANUARY: Lazy = Lazy::new(|| env::var("JANUARY_PUBLIC_URL").is_ok()); +pub static USE_VOSO: Lazy = Lazy::new(|| env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok()); - // SMTP Settings - pub static ref SMTP_HOST: String = - env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string()); - pub static ref SMTP_USERNAME: String = - env::var("REVOLT_SMTP_USERNAME").unwrap_or_else(|_| "".to_string()); - pub static ref SMTP_PASSWORD: String = - env::var("REVOLT_SMTP_PASSWORD").unwrap_or_else(|_| "".to_string()); - pub static ref SMTP_FROM: String = env::var("REVOLT_SMTP_FROM").unwrap_or_else(|_| "".to_string()); +// SMTP Settings +pub static SMTP_HOST: Lazy = Lazy::new(|| env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string())); +pub static SMTP_USERNAME: Lazy = Lazy::new(|| env::var("REVOLT_SMTP_USERNAME").unwrap_or_else(|_| "".to_string())); +pub static SMTP_PASSWORD: Lazy = Lazy::new(|| env::var("REVOLT_SMTP_PASSWORD").unwrap_or_else(|_| "".to_string())); +pub static SMTP_FROM: Lazy = Lazy::new(|| env::var("REVOLT_SMTP_FROM").unwrap_or_else(|_| "".to_string())); - // Application Logic Settings - pub static ref MAX_GROUP_SIZE: usize = - env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap(); - pub static ref MAX_BOT_COUNT: usize = - env::var("REVOLT_MAX_BOT_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap(); - pub static ref MAX_EMBED_COUNT: usize = - env::var("REVOLT_MAX_EMBED_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap(); - pub static ref MAX_SERVER_COUNT: usize = - env::var("REVOLT_MAX_SERVER_COUNT").unwrap_or_else(|_| "100".to_string()).parse().unwrap(); - pub static ref EARLY_ADOPTER_BADGE: i64 = - env::var("REVOLT_EARLY_ADOPTER_BADGE").unwrap_or_else(|_| "0".to_string()).parse().unwrap(); -} +// Application Logic Settings +pub static MAX_GROUP_SIZE: Lazy = Lazy::new(|| env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap()); +pub static MAX_BOT_COUNT: Lazy = Lazy::new(|| env::var("REVOLT_MAX_BOT_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap()); +pub static MAX_EMBED_COUNT: Lazy = Lazy::new(|| env::var("REVOLT_MAX_EMBED_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap()); +pub static MAX_SERVER_COUNT: Lazy = Lazy::new(|| env::var("REVOLT_MAX_SERVER_COUNT").unwrap_or_else(|_| "100".to_string()).parse().unwrap()); +pub static EARLY_ADOPTER_BADGE: Lazy = Lazy::new(|| env::var("REVOLT_EARLY_ADOPTER_BADGE").unwrap_or_else(|_| "0".to_string()).parse().unwrap()); pub fn preflight_checks() { format!("url = {}", *APP_URL); diff --git a/crates/quark/src/web/idempotency.rs b/crates/quark/src/web/idempotency.rs index 769341e7..49004a3a 100644 --- a/crates/quark/src/web/idempotency.rs +++ b/crates/quark/src/web/idempotency.rs @@ -9,6 +9,7 @@ use rocket::request::{FromRequest, Outcome}; use schemars::schema::{InstanceType, SchemaObject, SingleOrVec}; use serde::{Deserialize, Serialize}; use validator::Validate; +use once_cell::sync::Lazy; #[derive(Validate, Serialize, Deserialize)] pub struct IdempotencyKey { @@ -16,9 +17,7 @@ pub struct IdempotencyKey { key: String, } -lazy_static! { - static ref TOKEN_CACHE: Mutex> = Mutex::new(lru::LruCache::new(100)); -} +static TOKEN_CACHE: Lazy>> = Lazy::new(|| Mutex::new(lru::LruCache::new(100))); impl IdempotencyKey { // Backwards compatibility. diff --git a/crates/quark/src/web/ratelimiter.rs b/crates/quark/src/web/ratelimiter.rs index 631fa368..70249842 100644 --- a/crates/quark/src/web/ratelimiter.rs +++ b/crates/quark/src/web/ratelimiter.rs @@ -22,17 +22,16 @@ use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; use serde::Serialize; use dashmap::DashMap; +use once_cell::sync::Lazy; /// Ratelimit Bucket -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] struct Entry { used: u8, reset: u128, } -lazy_static! { - static ref MAP: DashMap = DashMap::new(); -} +static MAP: Lazy> = Lazy::new(DashMap::new); /// Get the current time from Unix Epoch as a Duration fn now() -> Duration { @@ -51,7 +50,7 @@ impl Entry { } /// Deduct one unit from the bucket and save - pub fn deduct(mut self) { + pub fn deduct(&mut self) { let current_time = now().as_millis(); if current_time > self.reset { self.used = 1; @@ -193,7 +192,7 @@ impl Ratelimiter { let key = key.finish(); let limit = resolve_bucket_limit(bucket); - let entry = Entry::from(key); + let mut entry = Entry::from(key); let remaining = entry.get_remaining(limit); if remaining > 0 {