From 301676fb540336268cb2190a56320d0c1e49d8b1 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 7 Apr 2024 22:57:55 +0100 Subject: [PATCH] refactor(quark): port report_content & code clean-up #283 --- crates/core/database/src/events/client.rs | 6 +- .../src/models/safety_reports/model.rs | 128 +-------------- .../src/models/safety_snapshots/model.rs | 88 ++++++++++- crates/core/database/src/util/bridge/v0.rs | 13 ++ crates/core/models/src/v0/channel_invites.rs | 1 + crates/core/models/src/v0/mod.rs | 2 + crates/core/models/src/v0/safety_reports.rs | 148 ++++++++++++++++++ crates/core/permissions/src/models/mod.rs | 6 +- .../delta/src/routes/invites/invite_join.rs | 2 +- .../delta/src/routes/safety/report_content.rs | 33 ++-- crates/delta/src/routes/servers/ban_create.rs | 2 +- .../delta/src/routes/servers/roles_create.rs | 2 +- .../delta/src/routes/servers/roles_delete.rs | 2 +- crates/delta/src/routes/servers/roles_edit.rs | 2 +- 14 files changed, 281 insertions(+), 154 deletions(-) create mode 100644 crates/core/models/src/v0/safety_reports.rs diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs index de63a9d4..173d39da 100644 --- a/crates/core/database/src/events/client.rs +++ b/crates/core/database/src/events/client.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use revolt_models::v0::{ AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember, - PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, User, + PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User, UserSettings, Webhook, }; use revolt_result::Error; @@ -176,8 +176,8 @@ pub enum EventV1 { /// Delete emoji EmojiDelete { id: String }, - /*/// New report - ReportCreate(Report), */ + /// New report + ReportCreate(Report), /// New channel ChannelCreate(Channel), diff --git a/crates/core/database/src/models/safety_reports/model.rs b/crates/core/database/src/models/safety_reports/model.rs index 0d8ee5d7..72fd28a6 100644 --- a/crates/core/database/src/models/safety_reports/model.rs +++ b/crates/core/database/src/models/safety_reports/model.rs @@ -1,4 +1,4 @@ -use iso8601_timestamp::Timestamp; +use revolt_models::v0::{ReportStatus, ReportedContent}; auto_derived!( /// User-generated platform moderation report @@ -19,130 +19,4 @@ auto_derived!( #[serde(default)] pub notes: String, } - - /// Reason for reporting content (message or server) - pub enum ContentReportReason { - /// No reason has been specified - NoneSpecified, - - /// Illegal content catch-all reason - Illegal, - - /// Selling or facilitating use of drugs or other illegal goods - IllegalGoods, - - /// Extortion or blackmail - IllegalExtortion, - - /// Revenge or child pornography - IllegalPornography, - - /// Illegal hacking activity - IllegalHacking, - - /// Extreme violence, gore, or animal cruelty - /// With exception to violence potrayed in media / creative arts - ExtremeViolence, - - /// Content that promotes harm to others / self - PromotesHarm, - - /// Unsolicited advertisements - UnsolicitedSpam, - - /// This is a raid - Raid, - - /// Spam or platform abuse - SpamAbuse, - - /// Scams or fraud - ScamsFraud, - - /// Distribution of malware or malicious links - Malware, - - /// Harassment or abuse targeted at another user - Harassment, - } - - /// Reason for reporting a user - pub enum UserReportReason { - /// No reason has been specified - NoneSpecified, - - /// Unsolicited advertisements - UnsolicitedSpam, - - /// User is sending spam or otherwise abusing the platform - SpamAbuse, - - /// User's profile contains inappropriate content for a general audience - InappropriateProfile, - - /// User is impersonating another user - Impersonation, - - /// User is evading a ban - BanEvasion, - - /// User is not of minimum age to use the platform - Underage, - } - - /// The content being reported - #[serde(tag = "type")] - pub enum ReportedContent { - /// Report a message - Message { - /// ID of the message - id: String, - /// Reason for reporting message - report_reason: ContentReportReason, - }, - /// Report a server - Server { - /// ID of the server - id: String, - /// Reason for reporting server - report_reason: ContentReportReason, - }, - /// Report a user - User { - /// ID of the user - id: String, - /// Reason for reporting a user - report_reason: UserReportReason, - /// Message context - message_id: Option, - }, - } - - /// Status of the report - #[serde(tag = "status")] - pub enum ReportStatus { - /// Report is waiting for triage / action - Created {}, - - /// Report was rejected - Rejected { - rejection_reason: String, - closed_at: Option, - }, - - /// Report was actioned and resolved - Resolved { closed_at: Option }, - } - - /// Just the status of the report - pub enum ReportStatusString { - /// Report is waiting for triage / action - Created, - - /// Report was rejected - Rejected, - - /// Report was actioned and resolved - Resolved, - } ); diff --git a/crates/core/database/src/models/safety_snapshots/model.rs b/crates/core/database/src/models/safety_snapshots/model.rs index 4cf861f3..e82ab388 100644 --- a/crates/core/database/src/models/safety_snapshots/model.rs +++ b/crates/core/database/src/models/safety_snapshots/model.rs @@ -1,4 +1,7 @@ -use revolt_models::v0::{Message, Server, User}; +use revolt_models::v0::MessageSort; +use revolt_result::Result; + +use crate::{Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, Server, User}; auto_derived!( /// Snapshot of some content @@ -33,3 +36,86 @@ auto_derived!( User(User), } ); + +impl SnapshotContent { + /// Generate snapshot from a given message + pub async fn generate_from_message( + db: &Database, + message: Message, + ) -> Result<(SnapshotContent, Vec)> { + // Collect message attachments + let files = message + .attachments + .as_ref() + .map(|attachments| attachments.iter().map(|x| x.id.to_string()).collect()) + .unwrap_or_default(); + + // Collect prior context + let prior_context = db + .fetch_messages(MessageQuery { + filter: MessageFilter { + channel: Some(message.channel.to_string()), + ..Default::default() + }, + limit: Some(15), + time_period: MessageTimePeriod::Absolute { + before: Some(message.id.to_string()), + after: None, + sort: Some(MessageSort::Latest), + }, + }) + .await?; + + // Collect leading context + let leading_context = db + .fetch_messages(MessageQuery { + filter: MessageFilter { + channel: Some(message.channel.to_string()), + ..Default::default() + }, + limit: Some(15), + time_period: MessageTimePeriod::Absolute { + before: None, + after: Some(message.id.to_string()), + sort: Some(MessageSort::Oldest), + }, + }) + .await?; + + Ok(( + SnapshotContent::Message { + message, + prior_context: prior_context.into_iter().map(Into::into).collect(), + leading_context: leading_context.into_iter().map(Into::into).collect(), + }, + files, + )) + } + + /// Generate snapshot from a given server + pub fn generate_from_server(server: Server) -> Result<(SnapshotContent, Vec)> { + // Collect server's icon and banner + let files = [&server.icon, &server.banner] + .iter() + .filter_map(|x| x.as_ref().map(|x| x.id.to_string())) + .collect(); + + Ok((SnapshotContent::Server(server), files)) + } + + /// Generate snapshot from a given user + pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec)> { + // Collect user's avatar and profile background + let files = [ + user.avatar.as_ref(), + user.profile + .as_ref() + .and_then(|profile| profile.background.as_ref()), + ] + .iter() + .filter_map(|x| x.as_ref().map(|x| x.id.to_string())) + .collect(); + + Ok((SnapshotContent::User(user), files)) + } +} diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index 5509589a..fd715395 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -445,6 +445,19 @@ impl From for crate::Masquerade { } } +impl From for Report { + fn from(value: crate::Report) -> Self { + Report { + id: value.id, + author_id: value.author_id, + content: value.content, + additional_context: value.additional_context, + status: value.status, + notes: value.notes, + } + } +} + impl From for ServerBan { fn from(value: crate::ServerBan) -> Self { ServerBan { diff --git a/crates/core/models/src/v0/channel_invites.rs b/crates/core/models/src/v0/channel_invites.rs index 32a3d084..f0e889d5 100644 --- a/crates/core/models/src/v0/channel_invites.rs +++ b/crates/core/models/src/v0/channel_invites.rs @@ -84,6 +84,7 @@ auto_derived!( /// Invite join response #[serde(tag = "type")] + #[allow(clippy::large_enum_variant)] pub enum InviteJoinResponse { Server { /// Channels in the server diff --git a/crates/core/models/src/v0/mod.rs b/crates/core/models/src/v0/mod.rs index e4f07fbd..78540e32 100644 --- a/crates/core/models/src/v0/mod.rs +++ b/crates/core/models/src/v0/mod.rs @@ -7,6 +7,7 @@ mod embeds; mod emojis; mod files; mod messages; +mod safety_reports; mod server_bans; mod server_members; mod servers; @@ -22,6 +23,7 @@ pub use embeds::*; pub use emojis::*; pub use files::*; pub use messages::*; +pub use safety_reports::*; pub use server_bans::*; pub use server_members::*; pub use servers::*; diff --git a/crates/core/models/src/v0/safety_reports.rs b/crates/core/models/src/v0/safety_reports.rs new file mode 100644 index 00000000..0d8ee5d7 --- /dev/null +++ b/crates/core/models/src/v0/safety_reports.rs @@ -0,0 +1,148 @@ +use iso8601_timestamp::Timestamp; + +auto_derived!( + /// User-generated platform moderation report + pub struct Report { + /// Unique Id + #[serde(rename = "_id")] + pub id: String, + /// Id of the user creating this report + pub author_id: String, + /// Reported content + pub content: ReportedContent, + /// Additional report context + pub additional_context: String, + /// Status of the report + #[serde(flatten)] + pub status: ReportStatus, + /// Additional notes included on the report + #[serde(default)] + pub notes: String, + } + + /// Reason for reporting content (message or server) + pub enum ContentReportReason { + /// No reason has been specified + NoneSpecified, + + /// Illegal content catch-all reason + Illegal, + + /// Selling or facilitating use of drugs or other illegal goods + IllegalGoods, + + /// Extortion or blackmail + IllegalExtortion, + + /// Revenge or child pornography + IllegalPornography, + + /// Illegal hacking activity + IllegalHacking, + + /// Extreme violence, gore, or animal cruelty + /// With exception to violence potrayed in media / creative arts + ExtremeViolence, + + /// Content that promotes harm to others / self + PromotesHarm, + + /// Unsolicited advertisements + UnsolicitedSpam, + + /// This is a raid + Raid, + + /// Spam or platform abuse + SpamAbuse, + + /// Scams or fraud + ScamsFraud, + + /// Distribution of malware or malicious links + Malware, + + /// Harassment or abuse targeted at another user + Harassment, + } + + /// Reason for reporting a user + pub enum UserReportReason { + /// No reason has been specified + NoneSpecified, + + /// Unsolicited advertisements + UnsolicitedSpam, + + /// User is sending spam or otherwise abusing the platform + SpamAbuse, + + /// User's profile contains inappropriate content for a general audience + InappropriateProfile, + + /// User is impersonating another user + Impersonation, + + /// User is evading a ban + BanEvasion, + + /// User is not of minimum age to use the platform + Underage, + } + + /// The content being reported + #[serde(tag = "type")] + pub enum ReportedContent { + /// Report a message + Message { + /// ID of the message + id: String, + /// Reason for reporting message + report_reason: ContentReportReason, + }, + /// Report a server + Server { + /// ID of the server + id: String, + /// Reason for reporting server + report_reason: ContentReportReason, + }, + /// Report a user + User { + /// ID of the user + id: String, + /// Reason for reporting a user + report_reason: UserReportReason, + /// Message context + message_id: Option, + }, + } + + /// Status of the report + #[serde(tag = "status")] + pub enum ReportStatus { + /// Report is waiting for triage / action + Created {}, + + /// Report was rejected + Rejected { + rejection_reason: String, + closed_at: Option, + }, + + /// Report was actioned and resolved + Resolved { closed_at: Option }, + } + + /// Just the status of the report + pub enum ReportStatusString { + /// Report is waiting for triage / action + Created, + + /// Report was rejected + Rejected, + + /// Report was actioned and resolved + Resolved, + } +); diff --git a/crates/core/permissions/src/models/mod.rs b/crates/core/permissions/src/models/mod.rs index 43f73cc9..330fe756 100644 --- a/crates/core/permissions/src/models/mod.rs +++ b/crates/core/permissions/src/models/mod.rs @@ -97,10 +97,8 @@ impl PermissionValue { { return Err(create_error!(CannotGiveMissingPermissions)); } - } else { - if !self.has(next_value.allows()) { - return Err(create_error!(CannotGiveMissingPermissions)); - } + } else if !self.has(next_value.allows()) { + return Err(create_error!(CannotGiveMissingPermissions)); } Ok(()) diff --git a/crates/delta/src/routes/invites/invite_join.rs b/crates/delta/src/routes/invites/invite_join.rs index b4e21ede..bfcb307b 100644 --- a/crates/delta/src/routes/invites/invite_join.rs +++ b/crates/delta/src/routes/invites/invite_join.rs @@ -36,7 +36,7 @@ pub async fn join( channel.add_user_to_group(db, &user, creator).await?; if let Channel::Group { recipients, .. } = &channel { Ok(Json(InviteJoinResponse::Group { - users: User::fetch_many_ids_as_mutuals(db, &user, &recipients).await?, + users: User::fetch_many_ids_as_mutuals(db, &user, recipients).await?, channel: channel.into(), })) } else { diff --git a/crates/delta/src/routes/safety/report_content.rs b/crates/delta/src/routes/safety/report_content.rs index 2856d225..1d5c2356 100644 --- a/crates/delta/src/routes/safety/report_content.rs +++ b/crates/delta/src/routes/safety/report_content.rs @@ -1,13 +1,11 @@ -use revolt_quark::events::client::EventV1; -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}; +use revolt_database::{events::client::EventV1, Database, Report, Snapshot, SnapshotContent, User}; +use revolt_models::v0::{ReportStatus, ReportedContent}; +use revolt_result::{create_error, Result}; use serde::Deserialize; use ulid::Ulid; use validator::Validate; -use rocket::serde::json::Json; +use rocket::{serde::json::Json, State}; /// # Report Data #[derive(Validate, Deserialize, JsonSchema)] @@ -25,14 +23,21 @@ pub struct DataReportContent { /// Report a piece of content to the moderation team. #[openapi(tag = "User Safety")] #[post("/report", data = "")] -pub async fn report_content(db: &Db, user: User, data: Json) -> Result<()> { +pub async fn report_content( + db: &State, + user: User, + data: Json, +) -> Result<()> { let data = data.into_inner(); - data.validate() - .map_err(|error| Error::FailedValidation { error })?; + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; // Bots cannot create reports if user.bot.is_some() { - return Err(Error::IsBot); + return Err(create_error!(IsBot)); } // Find the content and create a snapshot of it @@ -43,7 +48,7 @@ pub async fn report_content(db: &Db, user: User, data: Json) // Users cannot report themselves if message.author == user.id { - return Err(Error::CannotReportYourself); + return Err(create_error!(CannotReportYourself)); } let (snapshot, files) = SnapshotContent::generate_from_message(db, message).await?; @@ -54,7 +59,7 @@ pub async fn report_content(db: &Db, user: User, data: Json) // Users cannot report their own server if server.owner == user.id { - return Err(Error::CannotReportYourself); + return Err(create_error!(CannotReportYourself)); } let (snapshot, files) = SnapshotContent::generate_from_server(server)?; @@ -65,7 +70,7 @@ pub async fn report_content(db: &Db, user: User, data: Json) // Users cannot report themselves if reported_user.id == user.id { - return Err(Error::CannotReportYourself); + return Err(create_error!(CannotReportYourself)); } // Determine if there is a message provided as context @@ -122,7 +127,7 @@ pub async fn report_content(db: &Db, user: User, data: Json) db.insert_report(&report).await?; - EventV1::ReportCreate(report).global().await; + EventV1::ReportCreate(report.into()).global().await; Ok(()) } diff --git a/crates/delta/src/routes/servers/ban_create.rs b/crates/delta/src/routes/servers/ban_create.rs index 9c1951ad..8003e79e 100644 --- a/crates/delta/src/routes/servers/ban_create.rs +++ b/crates/delta/src/routes/servers/ban_create.rs @@ -48,7 +48,7 @@ pub async fn ban( // If member exists, check privileges against them if let Ok(member) = target.as_member(db, &server.id).await { - if member.get_ranking(&query.server_ref().as_ref().unwrap()) + if member.get_ranking(query.server_ref().as_ref().unwrap()) <= query.get_member_rank().unwrap_or(i64::MIN) { return Err(create_error!(NotElevated)); diff --git a/crates/delta/src/routes/servers/roles_create.rs b/crates/delta/src/routes/servers/roles_create.rs index a9e07d2b..dae18baf 100644 --- a/crates/delta/src/routes/servers/roles_create.rs +++ b/crates/delta/src/routes/servers/roles_create.rs @@ -31,7 +31,7 @@ pub async fn create( let mut query = DatabasePermissionQuery::new(db, &user).server(&server); calculate_server_permissions(&mut query) .await - .throw_if_lacking_channel_permission(ChannelPermission::ManageRole); + .throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?; let config = config().await; if server.roles.len() >= config.features.limits.default.server_roles { diff --git a/crates/delta/src/routes/servers/roles_delete.rs b/crates/delta/src/routes/servers/roles_delete.rs index e7df633a..9063dae2 100644 --- a/crates/delta/src/routes/servers/roles_delete.rs +++ b/crates/delta/src/routes/servers/roles_delete.rs @@ -22,7 +22,7 @@ pub async fn delete( let mut query = DatabasePermissionQuery::new(db, &user).server(&server); calculate_server_permissions(&mut query) .await - .throw_if_lacking_channel_permission(ChannelPermission::ManageRole); + .throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?; let member_rank = query.get_member_rank().unwrap_or(i64::MIN); diff --git a/crates/delta/src/routes/servers/roles_edit.rs b/crates/delta/src/routes/servers/roles_edit.rs index a75c011c..c7f5124c 100644 --- a/crates/delta/src/routes/servers/roles_edit.rs +++ b/crates/delta/src/routes/servers/roles_edit.rs @@ -32,7 +32,7 @@ pub async fn edit( let mut query = DatabasePermissionQuery::new(db, &user).server(&server); calculate_server_permissions(&mut query) .await - .throw_if_lacking_channel_permission(ChannelPermission::ManageRole); + .throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?; let member_rank = query.get_member_rank().unwrap_or(i64::MIN);