refactor(quark): port report_content & code clean-up

#283
This commit is contained in:
Paul Makles
2024-04-07 22:57:55 +01:00
parent 6b488f347e
commit 301676fb54
14 changed files with 281 additions and 154 deletions

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use revolt_models::v0::{ use revolt_models::v0::{
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer, AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember, FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, User, PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
UserSettings, Webhook, UserSettings, Webhook,
}; };
use revolt_result::Error; use revolt_result::Error;
@@ -176,8 +176,8 @@ pub enum EventV1 {
/// Delete emoji /// Delete emoji
EmojiDelete { id: String }, EmojiDelete { id: String },
/*/// New report /// New report
ReportCreate(Report), */ ReportCreate(Report),
/// New channel /// New channel
ChannelCreate(Channel), ChannelCreate(Channel),

View File

@@ -1,4 +1,4 @@
use iso8601_timestamp::Timestamp; use revolt_models::v0::{ReportStatus, ReportedContent};
auto_derived!( auto_derived!(
/// User-generated platform moderation report /// User-generated platform moderation report
@@ -19,130 +19,4 @@ auto_derived!(
#[serde(default)] #[serde(default)]
pub notes: String, 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<String>,
},
}
/// 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<Timestamp>,
},
/// Report was actioned and resolved
Resolved { closed_at: Option<Timestamp> },
}
/// 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,
}
); );

View File

@@ -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!( auto_derived!(
/// Snapshot of some content /// Snapshot of some content
@@ -33,3 +36,86 @@ auto_derived!(
User(User), User(User),
} }
); );
impl SnapshotContent {
/// Generate snapshot from a given message
pub async fn generate_from_message(
db: &Database,
message: Message,
) -> Result<(SnapshotContent, Vec<String>)> {
// Collect message attachments
let files = message
.attachments
.as_ref()
.map(|attachments| attachments.iter().map(|x| x.id.to_string()).collect())
.unwrap_or_default();
// Collect prior context
let prior_context = db
.fetch_messages(MessageQuery {
filter: MessageFilter {
channel: Some(message.channel.to_string()),
..Default::default()
},
limit: Some(15),
time_period: MessageTimePeriod::Absolute {
before: Some(message.id.to_string()),
after: None,
sort: Some(MessageSort::Latest),
},
})
.await?;
// Collect leading context
let leading_context = db
.fetch_messages(MessageQuery {
filter: MessageFilter {
channel: Some(message.channel.to_string()),
..Default::default()
},
limit: Some(15),
time_period: MessageTimePeriod::Absolute {
before: None,
after: Some(message.id.to_string()),
sort: Some(MessageSort::Oldest),
},
})
.await?;
Ok((
SnapshotContent::Message {
message,
prior_context: 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<String>)> {
// Collect server's icon and banner
let files = [&server.icon, &server.banner]
.iter()
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
.collect();
Ok((SnapshotContent::Server(server), files))
}
/// Generate snapshot from a given user
pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec<String>)> {
// Collect user's avatar and profile background
let files = [
user.avatar.as_ref(),
user.profile
.as_ref()
.and_then(|profile| profile.background.as_ref()),
]
.iter()
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
.collect();
Ok((SnapshotContent::User(user), files))
}
}

View File

@@ -445,6 +445,19 @@ impl From<Masquerade> for crate::Masquerade {
} }
} }
impl From<crate::Report> 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<crate::ServerBan> for ServerBan { impl From<crate::ServerBan> for ServerBan {
fn from(value: crate::ServerBan) -> Self { fn from(value: crate::ServerBan) -> Self {
ServerBan { ServerBan {

View File

@@ -84,6 +84,7 @@ auto_derived!(
/// Invite join response /// Invite join response
#[serde(tag = "type")] #[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum InviteJoinResponse { pub enum InviteJoinResponse {
Server { Server {
/// Channels in the server /// Channels in the server

View File

@@ -7,6 +7,7 @@ mod embeds;
mod emojis; mod emojis;
mod files; mod files;
mod messages; mod messages;
mod safety_reports;
mod server_bans; mod server_bans;
mod server_members; mod server_members;
mod servers; mod servers;
@@ -22,6 +23,7 @@ pub use embeds::*;
pub use emojis::*; pub use emojis::*;
pub use files::*; pub use files::*;
pub use messages::*; pub use messages::*;
pub use safety_reports::*;
pub use server_bans::*; pub use server_bans::*;
pub use server_members::*; pub use server_members::*;
pub use servers::*; pub use servers::*;

View File

@@ -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<String>,
},
}
/// 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<Timestamp>,
},
/// Report was actioned and resolved
Resolved { closed_at: Option<Timestamp> },
}
/// 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,
}
);

View File

@@ -97,10 +97,8 @@ impl PermissionValue {
{ {
return Err(create_error!(CannotGiveMissingPermissions)); return Err(create_error!(CannotGiveMissingPermissions));
} }
} else { } else if !self.has(next_value.allows()) {
if !self.has(next_value.allows()) { return Err(create_error!(CannotGiveMissingPermissions));
return Err(create_error!(CannotGiveMissingPermissions));
}
} }
Ok(()) Ok(())

View File

@@ -36,7 +36,7 @@ pub async fn join(
channel.add_user_to_group(db, &user, creator).await?; channel.add_user_to_group(db, &user, creator).await?;
if let Channel::Group { recipients, .. } = &channel { if let Channel::Group { recipients, .. } = &channel {
Ok(Json(InviteJoinResponse::Group { 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(), channel: channel.into(),
})) }))
} else { } else {

View File

@@ -1,13 +1,11 @@
use revolt_quark::events::client::EventV1; use revolt_database::{events::client::EventV1, Database, Report, Snapshot, SnapshotContent, User};
use revolt_quark::models::report::{ReportStatus, ReportedContent}; use revolt_models::v0::{ReportStatus, ReportedContent};
use revolt_quark::models::snapshot::{Snapshot, SnapshotContent}; use revolt_result::{create_error, Result};
use revolt_quark::models::{Report, User};
use revolt_quark::{Db, Error, Result};
use serde::Deserialize; use serde::Deserialize;
use ulid::Ulid; use ulid::Ulid;
use validator::Validate; use validator::Validate;
use rocket::serde::json::Json; use rocket::{serde::json::Json, State};
/// # Report Data /// # Report Data
#[derive(Validate, Deserialize, JsonSchema)] #[derive(Validate, Deserialize, JsonSchema)]
@@ -25,14 +23,21 @@ pub struct DataReportContent {
/// Report a piece of content to the moderation team. /// Report a piece of content to the moderation team.
#[openapi(tag = "User Safety")] #[openapi(tag = "User Safety")]
#[post("/report", data = "<data>")] #[post("/report", data = "<data>")]
pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>) -> Result<()> { pub async fn report_content(
db: &State<Database>,
user: User,
data: Json<DataReportContent>,
) -> Result<()> {
let data = data.into_inner(); let data = data.into_inner();
data.validate() data.validate().map_err(|error| {
.map_err(|error| Error::FailedValidation { error })?; create_error!(FailedValidation {
error: error.to_string()
})
})?;
// Bots cannot create reports // Bots cannot create reports
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot); return Err(create_error!(IsBot));
} }
// Find the content and create a snapshot of it // Find the content and create a snapshot of it
@@ -43,7 +48,7 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
// Users cannot report themselves // Users cannot report themselves
if message.author == user.id { if message.author == user.id {
return Err(Error::CannotReportYourself); return Err(create_error!(CannotReportYourself));
} }
let (snapshot, files) = SnapshotContent::generate_from_message(db, message).await?; 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<DataReportContent>)
// Users cannot report their own server // Users cannot report their own server
if server.owner == user.id { if server.owner == user.id {
return Err(Error::CannotReportYourself); return Err(create_error!(CannotReportYourself));
} }
let (snapshot, files) = SnapshotContent::generate_from_server(server)?; let (snapshot, files) = SnapshotContent::generate_from_server(server)?;
@@ -65,7 +70,7 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
// Users cannot report themselves // Users cannot report themselves
if reported_user.id == user.id { if reported_user.id == user.id {
return Err(Error::CannotReportYourself); return Err(create_error!(CannotReportYourself));
} }
// Determine if there is a message provided as context // Determine if there is a message provided as context
@@ -122,7 +127,7 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
db.insert_report(&report).await?; db.insert_report(&report).await?;
EventV1::ReportCreate(report).global().await; EventV1::ReportCreate(report.into()).global().await;
Ok(()) Ok(())
} }

View File

@@ -48,7 +48,7 @@ pub async fn ban(
// If member exists, check privileges against them // If member exists, check privileges against them
if let Ok(member) = target.as_member(db, &server.id).await { 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) <= query.get_member_rank().unwrap_or(i64::MIN)
{ {
return Err(create_error!(NotElevated)); return Err(create_error!(NotElevated));

View File

@@ -31,7 +31,7 @@ pub async fn create(
let mut query = DatabasePermissionQuery::new(db, &user).server(&server); let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query) calculate_server_permissions(&mut query)
.await .await
.throw_if_lacking_channel_permission(ChannelPermission::ManageRole); .throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?;
let config = config().await; let config = config().await;
if server.roles.len() >= config.features.limits.default.server_roles { if server.roles.len() >= config.features.limits.default.server_roles {

View File

@@ -22,7 +22,7 @@ pub async fn delete(
let mut query = DatabasePermissionQuery::new(db, &user).server(&server); let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query) calculate_server_permissions(&mut query)
.await .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); let member_rank = query.get_member_rank().unwrap_or(i64::MIN);

View File

@@ -32,7 +32,7 @@ pub async fn edit(
let mut query = DatabasePermissionQuery::new(db, &user).server(&server); let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query) calculate_server_permissions(&mut query)
.await .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); let member_rank = query.get_member_rank().unwrap_or(i64::MIN);