Compare commits

...
6 Commits
18 changed files with 379 additions and 176 deletions

No files matched your search

@@ -76,7 +76,7 @@ auto_derived!(
/// User's active status /// User's active status
pub struct UserStatus { pub struct UserStatus {
/// Custom status text /// Custom status text
#[serde(skip_serializing_if = "String::is_empty")] #[serde(skip_serializing_if = "String::is_empty", default)]
pub text: String, pub text: String,
/// Current presence option /// Current presence option
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -86,7 +86,7 @@ auto_derived!(
/// User's profile /// User's profile
pub struct UserProfile { pub struct UserProfile {
/// Text content on user's profile /// Text content on user's profile
#[serde(skip_serializing_if = "String::is_empty")] #[serde(skip_serializing_if = "String::is_empty", default)]
pub content: String, pub content: String,
/// Background visible on user's profile /// Background visible on user's profile
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -11,10 +11,6 @@ use rocket::serde::json::Json;
#[openapi(tag = "Groups")] #[openapi(tag = "Groups")]
#[get("/<target>/members")] #[get("/<target>/members")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<User>>> { pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<User>>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
perms(&user) perms(&user)
.channel(&channel) .channel(&channel)
@@ -1,82 +0,0 @@
use std::collections::HashSet;
use revolt_quark::models::snapshot::{SnapshotContent, SnapshotWithContext};
use revolt_quark::models::{Channel, User};
use revolt_quark::{Db, Error, Result};
use rocket::serde::json::Json;
/// # Fetch Snapshot
///
/// Fetch a snapshot for a given report
#[openapi(tag = "User Safety")]
#[get("/snapshot/<report_id>")]
pub async fn fetch_snapshot(
db: &Db,
user: User,
report_id: String,
) -> Result<Json<SnapshotWithContext>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
// Fetch snapshot
let snapshot = db.fetch_snapshot(&report_id).await?;
// Resolve and fetch IDs of associated content
let mut user_ids: HashSet<&str> = HashSet::new();
let mut channel_ids: HashSet<&str> = HashSet::new();
match &snapshot.content {
SnapshotContent::Message {
prior_context,
leading_context,
message,
} => {
for msg in prior_context {
user_ids.insert(&msg.author);
}
for msg in leading_context {
user_ids.insert(&msg.author);
}
user_ids.insert(&message.author);
channel_ids.insert(&message.channel);
}
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<String> = user_ids.into_iter().map(|s| s.to_owned()).collect();
let channel_ids: Vec<String> = channel_ids.into_iter().map(|s| s.to_owned()).collect();
// Fetch users and channels
let users = db.fetch_users(&user_ids).await?;
let channels = db.fetch_channels(&channel_ids).await?;
// Pull out first server from channels if possible
let server = if let Some(server_id) = channels.iter().find_map(|channel| match channel {
Channel::TextChannel { server, .. } => Some(server),
_ => None,
}) {
Some(db.fetch_server(server_id).await?)
} else {
None
};
// Return snapshot with context
Ok(Json(SnapshotWithContext {
snapshot,
users,
channels,
server,
}))
}
@@ -0,0 +1,87 @@
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 Snapshots
///
/// Fetch a snapshots for a given report
#[openapi(tag = "User Safety")]
#[get("/snapshot/<report_id>")]
pub async fn fetch_snapshots(
db: &Db,
user: User,
report_id: String,
) -> Result<Json<Vec<SnapshotWithContext>>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
// Fetch snapshots
let snapshots = db.fetch_snapshots(&report_id).await?;
let mut result = vec![];
for snapshot in snapshots {
// 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<String> = user_ids.into_iter().map(|s| s.to_owned()).collect();
let channel_ids: Vec<String> = channel_ids.into_iter().map(|s| s.to_owned()).collect();
// Fetch users and channels
let users = db.fetch_users(&user_ids).await?;
let channels = db.fetch_channels(&channel_ids).await?;
// Pull out first server from channels if possible
let server = if let Some(server_id) = channels.iter().find_map(|channel| match channel {
Channel::TextChannel { server, .. } => Some(server),
_ => None,
}) {
Some(db.fetch_server(server_id).await?)
} else {
None
};
// Return snapshot with context
result.push(SnapshotWithContext {
snapshot,
users,
channels,
server,
});
}
Ok(Json(result))
}
+2 -2
View File
@@ -6,7 +6,7 @@ mod fetch_report;
mod fetch_reports; mod fetch_reports;
mod report_content; mod report_content;
mod fetch_snapshot; mod fetch_snapshots;
pub fn routes() -> (Vec<Route>, OpenApi) { pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![ openapi_get_routes_spec![
@@ -16,6 +16,6 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
fetch_reports::fetch_reports, fetch_reports::fetch_reports,
report_content::report_content, report_content::report_content,
// Snapshots // Snapshots
fetch_snapshot::fetch_snapshot fetch_snapshots::fetch_snapshots
] ]
} }
@@ -1,5 +1,4 @@
use revolt_quark::events::client::EventV1; use revolt_quark::events::client::EventV1;
use revolt_quark::models::message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod};
use revolt_quark::models::report::{ReportStatus, ReportedContent}; use revolt_quark::models::report::{ReportStatus, ReportedContent};
use revolt_quark::models::snapshot::{Snapshot, SnapshotContent}; use revolt_quark::models::snapshot::{Snapshot, SnapshotContent};
use revolt_quark::models::{Report, User}; use revolt_quark::models::{Report, User};
@@ -38,7 +37,7 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
// Find the content and create a snapshot of it // Find the content and create a snapshot of it
// Also retrieve any references to Files // Also retrieve any references to Files
let (content, files): (SnapshotContent, Vec<String>) = match &data.content { let (snapshots, files): (Vec<SnapshotContent>, Vec<String>) = match &data.content {
ReportedContent::Message { id, .. } => { ReportedContent::Message { id, .. } => {
let message = db.fetch_message(id).await?; let message = db.fetch_message(id).await?;
@@ -47,53 +46,8 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
return Err(Error::CannotReportYourself); return Err(Error::CannotReportYourself);
} }
// Collect message attachments let (snapshot, files) = SnapshotContent::generate_from_message(db, message).await?;
let files = message (vec![snapshot], files)
.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?;
(
SnapshotContent::Message {
message,
prior_context,
leading_context,
},
files,
)
} }
ReportedContent::Server { id, .. } => { ReportedContent::Server { id, .. } => {
let server = db.fetch_server(id).await?; let server = db.fetch_server(id).await?;
@@ -103,15 +57,10 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
return Err(Error::CannotReportYourself); return Err(Error::CannotReportYourself);
} }
// Collect server's icon and banner let (snapshot, files) = SnapshotContent::generate_from_server(db, server)?;
let files = [&server.icon, &server.banner] (vec![snapshot], files)
.iter()
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
.collect();
(SnapshotContent::Server(server), files)
} }
ReportedContent::User { id, .. } => { ReportedContent::User { id, message_id, .. } => {
let reported_user = db.fetch_user(id).await?; let reported_user = db.fetch_user(id).await?;
// Users cannot report themselves // Users cannot report themselves
@@ -119,19 +68,25 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
return Err(Error::CannotReportYourself); return Err(Error::CannotReportYourself);
} }
// Collect user's avatar and profile background // Determine if there is a message provided as context
let files = [ let message = if let Some(id) = message_id {
reported_user.avatar.as_ref(), db.fetch_message(id).await.ok()
reported_user } else {
.profile None
.as_ref() };
.and_then(|profile| profile.background.as_ref()),
]
.iter()
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
.collect();
(SnapshotContent::User(reported_user), files) let (snapshot, files) = SnapshotContent::generate_from_user(db, reported_user)?;
if let Some(message) = message {
let (message_snapshot, message_files) =
SnapshotContent::generate_from_message(db, message).await?;
(
vec![snapshot, message_snapshot],
[files, message_files].concat(),
)
} else {
(vec![snapshot], files)
}
} }
}; };
@@ -143,14 +98,17 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
// Generate an id for the report // Generate an id for the report
let id = Ulid::new().to_string(); let id = Ulid::new().to_string();
// Save a snapshot of the content // Insert all new generated snapshots
let snapshot = Snapshot { for content in snapshots {
id: Ulid::new().to_string(), // Save a snapshot of the content
report_id: id.to_string(), let snapshot = Snapshot {
content, id: Ulid::new().to_string(),
}; report_id: id.to_string(),
content,
};
db.insert_snapshot(&snapshot).await?; db.insert_snapshot(&snapshot).await?;
}
// Save the report // Save the report
let report = Report { let report = Report {
@@ -0,0 +1,82 @@
use revolt_quark::{
models::{Member, User},
perms, Db, Error, Ref, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
/// # Query Parameters
#[derive(Deserialize, JsonSchema, FromForm)]
pub struct OptionsQueryMembers {
/// String to search for
query: String,
/// Discourage use of this API
experimental_api: bool,
}
/// # Query members by name
#[derive(Serialize, JsonSchema)]
pub struct MemberQueryResponse {
/// List of members
members: Vec<Member>,
/// List of users
users: Vec<User>,
}
/// # Query members by name
///
/// Query members by a given name, this API is not stable and will be removed in the future.
#[openapi(tag = "Server Members")]
#[get("/<target>/members_experimental_query?<options..>")]
pub async fn member_experimental_query(
db: &Db,
user: User,
target: Ref,
options: OptionsQueryMembers,
) -> Result<Json<MemberQueryResponse>> {
if !options.experimental_api {
return Err(Error::InternalError);
}
let server = target.as_server(db).await?;
perms(&user).server(&server).calc(db).await?;
let mut members = db.fetch_all_members(&server.id).await?;
let mut user_ids = vec![];
for member in &members {
user_ids.push(member.id.user.clone());
}
let mut users = User::fetch_foreign_users(db, &user_ids).await?;
// Ensure the lists match up exactly
members.sort_by(|a, b| a.id.user.cmp(&b.id.user));
users.sort_by(|a, b| a.id.cmp(&b.id));
// Filter all matches
let mut zipped_vec: Vec<(Member, User)> = members
.into_iter()
.zip(users)
.filter(|(member, user)| {
if let Some(nickname) = &member.nickname {
nickname.contains(&options.query)
} else {
user.username.contains(&options.query)
}
})
.collect();
// Sort remaining matches by length
zipped_vec.sort_by(|(member_a, user_a), (member_b, user_b)| {
let left = member_a.nickname.as_ref().unwrap_or(&user_a.username);
let right = member_b.nickname.as_ref().unwrap_or(&user_b.username);
left.len().cmp(&right.len())
});
// Take the first five and return them
let (members, users) = zipped_vec.into_iter().take(10).unzip();
Ok(Json(MemberQueryResponse { members, users }))
}
+2
View File
@@ -8,6 +8,7 @@ mod channel_create;
mod emoji_list; mod emoji_list;
mod invites_fetch; mod invites_fetch;
mod member_edit; mod member_edit;
mod member_experimental_query;
mod member_fetch; mod member_fetch;
mod member_fetch_all; mod member_fetch_all;
mod member_remove; mod member_remove;
@@ -34,6 +35,7 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
member_remove::req, member_remove::req,
member_fetch::req, member_fetch::req,
member_edit::req, member_edit::req,
member_experimental_query::member_experimental_query,
ban_create::req, ban_create::req,
ban_remove::req, ban_remove::req,
ban_list::req, ban_list::req,
+1
View File
@@ -183,6 +183,7 @@ pub enum EventV1 {
id: String, id: String,
data: PartialUser, data: PartialUser,
clear: Vec<FieldsUser>, clear: Vec<FieldsUser>,
event_id: Option<String>,
}, },
/// Relationship with another user changed /// Relationship with another user changed
+13
View File
@@ -302,6 +302,7 @@ impl State {
..Default::default() ..Default::default()
}, },
clear: vec![], clear: vec![],
event_id: Some(ulid::Ulid::new().to_string()),
}; };
for server in self.cache.servers.keys() { for server in self.cache.servers.keys() {
@@ -499,6 +500,17 @@ impl State {
} }
} }
EventV1::UserUpdate { event_id, .. } => {
if let Some(id) = event_id {
if self.cache.seen_events.contains(id) {
return false;
}
self.cache.seen_events.put(id.to_string(), ());
}
*event_id = None;
}
EventV1::UserRelationship { id, user, .. } => { EventV1::UserRelationship { id, user, .. } => {
self.cache.users.insert(id.clone(), user.clone()); self.cache.users.insert(id.clone(), user.clone());
@@ -508,6 +520,7 @@ impl State {
self.remove_subscription(id); self.remove_subscription(id);
} }
} }
_ => {} _ => {}
} }
+20 -1
View File
@@ -1,5 +1,7 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use lru::LruCache;
use crate::models::{Channel, Member, Server, User}; use crate::models::{Channel, Member, Server, User};
/// Enumeration representing some change in subscriptions /// Enumeration representing some change in subscriptions
@@ -26,7 +28,7 @@ pub enum SubscriptionStateChange {
/// ------------------------------------------------ /// ------------------------------------------------
/// We can strip these objects to core information!! /// We can strip these objects to core information!!
/// ------------------------------------------------ /// ------------------------------------------------
#[derive(Debug, Default)] #[derive(Debug)]
pub struct Cache { pub struct Cache {
pub user_id: String, pub user_id: String,
@@ -34,6 +36,23 @@ pub struct Cache {
pub channels: HashMap<String, Channel>, pub channels: HashMap<String, Channel>,
pub members: HashMap<String, Member>, pub members: HashMap<String, Member>,
pub servers: HashMap<String, Server>, pub servers: HashMap<String, Server>,
pub seen_events: LruCache<String, ()>,
}
impl Default for Cache {
fn default() -> Self {
Cache {
user_id: Default::default(),
users: Default::default(),
channels: Default::default(),
members: Default::default(),
servers: Default::default(),
seen_events: LruCache::new(20),
}
}
} }
/// Client state /// Client state
@@ -10,7 +10,7 @@ impl AbstractSnapshot for DummyDb {
Ok(()) Ok(())
} }
async fn fetch_snapshot(&self, _report_id: &str) -> Result<Snapshot> { async fn fetch_snapshots(&self, _report_id: &str) -> Result<Vec<Snapshot>> {
todo!() todo!()
} }
} }
+1
View File
@@ -26,4 +26,5 @@ pub mod users {
pub mod safety { pub mod safety {
pub mod report; pub mod report;
pub mod snapshot;
} }
@@ -0,0 +1,91 @@
use crate::{
models::{
message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod},
snapshot::SnapshotContent,
Message, Server, User,
},
Database, Result,
};
impl SnapshotContent {
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,
leading_context,
},
files,
))
}
pub fn generate_from_server(
db: &Database,
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))
}
pub fn generate_from_user(db: &Database, 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))
}
}
@@ -33,6 +33,7 @@ impl User {
id: self.id.clone(), id: self.id.clone(),
data: partial, data: partial,
clear: remove, clear: remove,
event_id: Some(ulid::Ulid::new().to_string()),
} }
.p_user(self.id.clone(), db) .p_user(self.id.clone(), db)
.await; .await;
@@ -11,8 +11,8 @@ impl AbstractSnapshot for MongoDb {
self.insert_one(COL, snapshot).await.map(|_| ()) self.insert_one(COL, snapshot).await.map(|_| ())
} }
async fn fetch_snapshot(&self, report_id: &str) -> Result<Snapshot> { async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>> {
self.find_one( self.find(
COL, COL,
doc! { doc! {
"report_id": report_id "report_id": report_id
+38 -4
View File
@@ -1,3 +1,4 @@
use iso8601_timestamp::Timestamp;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Reason for reporting content (message or server) /// Reason for reporting content (message or server)
@@ -6,16 +7,41 @@ pub enum ContentReportReason {
/// No reason has been specified /// No reason has been specified
NoneSpecified, NoneSpecified,
/// Blatantly illegal content /// Illegal content catch-all reason
Illegal, 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 /// Content that promotes harm to others / self
PromotesHarm, PromotesHarm,
/// Unsolicited advertisements
UnsolicitedSpam,
/// This is a raid
Raid,
/// Spam or platform abuse /// Spam or platform abuse
SpamAbuse, SpamAbuse,
/// Distribution of malware /// Scams or fraud
ScamsFraud,
/// Distribution of malware or malicious links
Malware, Malware,
/// Harassment or abuse targeted at another user /// Harassment or abuse targeted at another user
@@ -28,6 +54,9 @@ pub enum UserReportReason {
/// No reason has been specified /// No reason has been specified
NoneSpecified, NoneSpecified,
/// Unsolicited advertisements
UnsolicitedSpam,
/// User is sending spam or otherwise abusing the platform /// User is sending spam or otherwise abusing the platform
SpamAbuse, SpamAbuse,
@@ -68,6 +97,8 @@ pub enum ReportedContent {
id: String, id: String,
/// Reason for reporting a user /// Reason for reporting a user
report_reason: UserReportReason, report_reason: UserReportReason,
/// Message context
message_id: Option<String>,
}, },
} }
@@ -79,10 +110,13 @@ pub enum ReportStatus {
Created {}, Created {},
/// Report was rejected /// Report was rejected
Rejected { rejection_reason: String }, Rejected {
rejection_reason: String,
closed_at: Option<Timestamp>,
},
/// Report was actioned and resolved /// Report was actioned and resolved
Resolved {}, Resolved { closed_at: Option<Timestamp> },
} }
/// User-generated platform moderation report. /// User-generated platform moderation report.
+2 -2
View File
@@ -6,6 +6,6 @@ pub trait AbstractSnapshot: Sync + Send {
/// Insert a new snapshot into the database /// Insert a new snapshot into the database
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>; async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>;
/// Fetch a snapshot by a report's id /// Fetch a snapshots by a report's id
async fn fetch_snapshot(&self, report_id: &str) -> Result<Snapshot>; async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>>;
} }