Compare commits

..
18 changed files with 379 additions and 176 deletions
@@ -76,7 +76,7 @@ auto_derived!(
/// User's active status
pub struct UserStatus {
/// Custom status text
#[serde(skip_serializing_if = "String::is_empty")]
#[serde(skip_serializing_if = "String::is_empty", default)]
pub text: String,
/// Current presence option
#[serde(skip_serializing_if = "Option::is_none")]
@@ -86,7 +86,7 @@ auto_derived!(
/// User's profile
pub struct UserProfile {
/// Text content on user's profile
#[serde(skip_serializing_if = "String::is_empty")]
#[serde(skip_serializing_if = "String::is_empty", default)]
pub content: String,
/// Background visible on user's profile
#[serde(skip_serializing_if = "Option::is_none")]
@@ -11,10 +11,6 @@ use rocket::serde::json::Json;
#[openapi(tag = "Groups")]
#[get("/<target>/members")]
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?;
perms(&user)
.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 report_content;
mod fetch_snapshot;
mod fetch_snapshots;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
@@ -16,6 +16,6 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
fetch_reports::fetch_reports,
report_content::report_content,
// Snapshots
fetch_snapshot::fetch_snapshot
fetch_snapshots::fetch_snapshots
]
}
@@ -1,5 +1,4 @@
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::snapshot::{Snapshot, SnapshotContent};
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
// 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, .. } => {
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);
}
// 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?;
(
SnapshotContent::Message {
message,
prior_context,
leading_context,
},
files,
)
let (snapshot, files) = SnapshotContent::generate_from_message(db, message).await?;
(vec![snapshot], files)
}
ReportedContent::Server { id, .. } => {
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);
}
// 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();
(SnapshotContent::Server(server), files)
let (snapshot, files) = SnapshotContent::generate_from_server(db, server)?;
(vec![snapshot], files)
}
ReportedContent::User { id, .. } => {
ReportedContent::User { id, message_id, .. } => {
let reported_user = db.fetch_user(id).await?;
// Users cannot report themselves
@@ -119,19 +68,25 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
return Err(Error::CannotReportYourself);
}
// Collect user's avatar and profile background
let files = [
reported_user.avatar.as_ref(),
reported_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();
// Determine if there is a message provided as context
let message = if let Some(id) = message_id {
db.fetch_message(id).await.ok()
} else {
None
};
(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
let id = Ulid::new().to_string();
// Save a snapshot of the content
let snapshot = Snapshot {
id: Ulid::new().to_string(),
report_id: id.to_string(),
content,
};
// Insert all new generated snapshots
for content in snapshots {
// Save a snapshot of the content
let snapshot = Snapshot {
id: Ulid::new().to_string(),
report_id: id.to_string(),
content,
};
db.insert_snapshot(&snapshot).await?;
db.insert_snapshot(&snapshot).await?;
}
// Save the 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 invites_fetch;
mod member_edit;
mod member_experimental_query;
mod member_fetch;
mod member_fetch_all;
mod member_remove;
@@ -34,6 +35,7 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
member_remove::req,
member_fetch::req,
member_edit::req,
member_experimental_query::member_experimental_query,
ban_create::req,
ban_remove::req,
ban_list::req,
+1
View File
@@ -183,6 +183,7 @@ pub enum EventV1 {
id: String,
data: PartialUser,
clear: Vec<FieldsUser>,
event_id: Option<String>,
},
/// Relationship with another user changed
+13
View File
@@ -302,6 +302,7 @@ impl State {
..Default::default()
},
clear: vec![],
event_id: Some(ulid::Ulid::new().to_string()),
};
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, .. } => {
self.cache.users.insert(id.clone(), user.clone());
@@ -508,6 +520,7 @@ impl State {
self.remove_subscription(id);
}
}
_ => {}
}
+20 -1
View File
@@ -1,5 +1,7 @@
use std::collections::{HashMap, HashSet};
use lru::LruCache;
use crate::models::{Channel, Member, Server, User};
/// Enumeration representing some change in subscriptions
@@ -26,7 +28,7 @@ pub enum SubscriptionStateChange {
/// ------------------------------------------------
/// We can strip these objects to core information!!
/// ------------------------------------------------
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct Cache {
pub user_id: String,
@@ -34,6 +36,23 @@ pub struct Cache {
pub channels: HashMap<String, Channel>,
pub members: HashMap<String, Member>,
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
@@ -10,7 +10,7 @@ impl AbstractSnapshot for DummyDb {
Ok(())
}
async fn fetch_snapshot(&self, _report_id: &str) -> Result<Snapshot> {
async fn fetch_snapshots(&self, _report_id: &str) -> Result<Vec<Snapshot>> {
todo!()
}
}
+1
View File
@@ -26,4 +26,5 @@ pub mod users {
pub mod safety {
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(),
data: partial,
clear: remove,
event_id: Some(ulid::Ulid::new().to_string()),
}
.p_user(self.id.clone(), db)
.await;
@@ -11,8 +11,8 @@ impl AbstractSnapshot for MongoDb {
self.insert_one(COL, snapshot).await.map(|_| ())
}
async fn fetch_snapshot(&self, report_id: &str) -> Result<Snapshot> {
self.find_one(
async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>> {
self.find(
COL,
doc! {
"report_id": report_id
+38 -4
View File
@@ -1,3 +1,4 @@
use iso8601_timestamp::Timestamp;
use serde::{Deserialize, Serialize};
/// Reason for reporting content (message or server)
@@ -6,16 +7,41 @@ pub enum ContentReportReason {
/// No reason has been specified
NoneSpecified,
/// Blatantly illegal content
/// 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,
/// Distribution of malware
/// Scams or fraud
ScamsFraud,
/// Distribution of malware or malicious links
Malware,
/// Harassment or abuse targeted at another user
@@ -28,6 +54,9 @@ pub enum UserReportReason {
/// No reason has been specified
NoneSpecified,
/// Unsolicited advertisements
UnsolicitedSpam,
/// User is sending spam or otherwise abusing the platform
SpamAbuse,
@@ -68,6 +97,8 @@ pub enum ReportedContent {
id: String,
/// Reason for reporting a user
report_reason: UserReportReason,
/// Message context
message_id: Option<String>,
},
}
@@ -79,10 +110,13 @@ pub enum ReportStatus {
Created {},
/// Report was rejected
Rejected { rejection_reason: String },
Rejected {
rejection_reason: String,
closed_at: Option<Timestamp>,
},
/// Report was actioned and resolved
Resolved {},
Resolved { closed_at: Option<Timestamp> },
}
/// 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
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<Snapshot>;
/// Fetch a snapshots by a report's id
async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>>;
}