Merge branch 'master' into webhooks

This commit is contained in:
Angelo Kontaxis
2023-04-01 22:52:11 +01:00
committed by GitHub
71 changed files with 1173 additions and 428 deletions

View File

@@ -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;

View File

@@ -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 = "<data>")]
pub async fn message_query(
db: &Db,
user: User,
data: Json<MessageQuery>,
) -> Result<Json<BulkMessageResponse>> {
// 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)
}

View File

@@ -0,0 +1,9 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod message_query;
mod stats;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![stats::stats, message_query::message_query]
}

View File

@@ -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<Json<Stats>> {
Ok(Json(db.generate_stats().await?))
}

View File

@@ -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<EmptyR
match &channel {
Channel::Group { .. } => {
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

View File

@@ -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<DataEditMessage>,
) -> Result<Json<Message>> {
@@ -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);
}

View File

@@ -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)
}

View File

@@ -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<String>,
/// 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)
}

View File

@@ -10,6 +10,7 @@ use revolt_quark::{
use rocket::serde::json::Json;
use validator::Validate;
use once_cell::sync::Lazy;
/// # Send Message
///

View File

@@ -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<Build>) -> Rocket<Build> {
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"
]
},

View File

@@ -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<ReportStatus>,
/// Report notes
notes: Option<String>,
}
/// # Edit Report
///
/// Edit a report.
#[openapi(tag = "User Safety")]
#[patch("/reports/<report>", data = "<edit>")]
pub async fn edit_report(
db: &Db,
user: User,
report: Ref,
edit: Json<DataEditReport>,
) -> Result<Json<Report>> {
// 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))
}

View File

@@ -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/<id>")]
pub async fn fetch_report(db: &Db, user: User, id: String) -> Result<Json<Report>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
db.fetch_report(&id).await.map(Json)
}

View File

@@ -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<Json<Vec<Report>>> {
// Must be privileged for this route
if !user.privileged {
return Err(Error::NotPrivileged);
}
db.fetch_reports().await.map(Json)
}

View File

@@ -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/<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,
}))
}

View File

@@ -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<Route>, 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
]
}

View File

@@ -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<DataReportContent>)
// 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<DataReportContent>)
author_id: user.id,
content: data.content,
additional_context: data.additional_context,
status: ReportStatus::Created {},
notes: String::new(),
};
db.insert_report(&report).await?;

View File

@@ -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 {

View File

@@ -33,6 +33,10 @@ pub struct DataEditServer {
/// System message configuration
system_messages: Option<SystemMessageChannels>,
/// Bitfield of server flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
// Whether this server is age-restricted
// nsfw: Option<bool>,
/// 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,

View File

@@ -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<String>,
/// New user status
#[validate]
status: Option<UserStatus>,
@@ -32,9 +36,14 @@ pub struct DataEditUser {
/// This is applied as a partial.
#[validate]
profile: Option<UserProfileData>,
/// Attachment Id for avatar
#[validate(length(min = 1, max = 128))]
avatar: Option<String>,
/// Bitfield of user badges
#[serde(skip_serializing_if = "Option::is_none")]
badges: Option<i32>,
/// Enum of user flags
#[serde(skip_serializing_if = "Option::is_none")]
flags: Option<i32>,
/// Fields to remove from user object
#[validate(length(min = 1))]
remove: Option<Vec<FieldsUser>>,
@@ -44,19 +53,36 @@ pub struct DataEditUser {
///
/// Edit currently authenticated user.
#[openapi(tag = "User Information")]
#[patch("/@me", data = "<data>")]
#[patch("/<target>", data = "<data>")]
pub async fn req(
db: &State<Database>,
mut user: User,
target: Ref,
data: Json<DataEditUser>,
) -> Result<Json<User>> {
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 {