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

14
Cargo.lock generated
View File

@@ -2106,9 +2106,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.13.0" version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
@@ -2170,7 +2170,7 @@ dependencies = [
[[package]] [[package]]
name = "optional_struct" name = "optional_struct"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/insertish/OptionalStruct?rev=e275d2726595474632485934aa0887fa52281f70#e275d2726595474632485934aa0887fa52281f70" source = "git+https://github.com/insertish/OptionalStruct?rev=ee56427cee1f007839825d93d07fffd5a5e038c7#ee56427cee1f007839825d93d07fffd5a5e038c7"
dependencies = [ dependencies = [
"quote 0.3.15", "quote 0.3.15",
"syn 0.11.11", "syn 0.11.11",
@@ -2792,7 +2792,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.5.9" version = "0.5.17"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
@@ -2808,7 +2808,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-delta" name = "revolt-delta"
version = "0.5.9" version = "0.5.17"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"async-std", "async-std",
@@ -2819,7 +2819,6 @@ dependencies = [
"env_logger", "env_logger",
"futures", "futures",
"impl_ops", "impl_ops",
"lazy_static",
"lettre", "lettre",
"linkify 0.6.0", "linkify 0.6.0",
"log", "log",
@@ -2848,7 +2847,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-quark" name = "revolt-quark"
version = "0.5.9" version = "0.5.17"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"async-recursion", "async-recursion",
@@ -2866,7 +2865,6 @@ dependencies = [
"impl_ops", "impl_ops",
"indexmap", "indexmap",
"iso8601-timestamp", "iso8601-timestamp",
"lazy_static",
"linkify 0.8.1", "linkify 0.8.1",
"log", "log",
"lru", "lru",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.5.9" version = "0.5.17"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.5.9" version = "0.5.17"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
@@ -15,9 +15,8 @@ log = "0.4.11"
dotenv = "0.15.0" dotenv = "0.15.0"
dashmap = "5.2.0" dashmap = "5.2.0"
linkify = "0.6.0" linkify = "0.6.0"
once_cell = "1.4.1" once_cell = "1.17.1"
env_logger = "0.7.1" env_logger = "0.7.1"
lazy_static = "1.4.0"
# Lang. Utilities # Lang. Utilities
regex = "1" regex = "1"

View File

@@ -8,7 +8,7 @@ fn main() {
.output() .output()
{ {
if let Ok(git_origin) = String::from_utf8(output.stdout) { if let Ok(git_origin) = String::from_utf8(output.stdout) {
println!("cargo:rustc-env=GIT_ORIGIN_URL={}", git_origin); println!("cargo:rustc-env=GIT_ORIGIN_URL={git_origin}");
} }
} }

View File

@@ -4,8 +4,6 @@ extern crate rocket;
extern crate revolt_rocket_okapi; extern crate revolt_rocket_okapi;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
#[macro_use]
extern crate lazy_static;
pub mod routes; pub mod routes;
pub mod util; 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::{ use revolt_quark::{
models::{Channel, User}, get_relationship,
models::{user::RelationshipStatus, Channel, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result, 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 { match &channel {
Channel::Group { .. } => { Channel::Group { .. } => {
let member = member.as_user(db).await?; let member = member.as_user(db).await?;
if !matches!(
get_relationship(&user, &member.id),
RelationshipStatus::Friend
) {
return Err(Error::NotFriends);
}
channel channel
.add_user_to_group(db, &member.id, &user.id) .add_user_to_group(db, &member.id, &user.id)
.await .await

View File

@@ -1,8 +1,9 @@
use revolt_quark::{ use revolt_quark::{
models::message::{PartialMessage, SendableEmbed}, models::message::{PartialMessage, SendableEmbed},
models::{Message, User}, models::{Message, User},
perms,
types::january::Embed, types::january::Embed,
Db, Error, Ref, Result, Timestamp, Db, Error, Permission, Ref, Result, Timestamp,
}; };
use rocket::serde::json::Json; use rocket::serde::json::Json;
@@ -28,7 +29,7 @@ pub struct DataEditMessage {
pub async fn req( pub async fn req(
db: &Db, db: &Db,
user: User, user: User,
target: String, target: Ref,
msg: Ref, msg: Ref,
edit: Json<DataEditMessage>, edit: Json<DataEditMessage>,
) -> Result<Json<Message>> { ) -> Result<Json<Message>> {
@@ -36,10 +37,17 @@ pub async fn req(
edit.validate() edit.validate()
.map_err(|error| Error::FailedValidation { error })?; .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)?; Message::validate_sum(&edit.content, &edit.embeds)?;
let mut message = msg.as_message(db).await?; let mut message = msg.as_message(db).await?;
if message.channel != target { if message.channel != channel.id() {
return Err(Error::NotFound); return Err(Error::NotFound);
} }

View File

@@ -1,6 +1,8 @@
use revolt_quark::{ use revolt_quark::{
models::{ models::{
message::{BulkMessageResponse, MessageSort}, message::{
BulkMessageResponse, MessageFilter, MessageQuery, MessageSort, MessageTimePeriod,
},
User, User,
}, },
perms, Db, Error, Permission, Ref, Result, perms, Db, Error, Permission, Ref, Result,
@@ -68,14 +70,28 @@ pub async fn req(
sort, sort,
nearby, nearby,
include_users, include_users,
..
} = options; } = options;
let messages = db 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?; .await?;
BulkMessageResponse::transform(db, &channel, messages, include_users) BulkMessageResponse::transform(db, Some(&channel), messages, include_users)
.await .await
.map(Json) .map(Json)
} }

View File

@@ -1,6 +1,8 @@
use revolt_quark::{ use revolt_quark::{
models::{ models::{
message::{BulkMessageResponse, MessageSort}, message::{
BulkMessageResponse, MessageFilter, MessageQuery, MessageSort, MessageTimePeriod,
},
User, User,
}, },
perms, Db, Error, Permission, Ref, Result, perms, Db, Error, Permission, Ref, Result,
@@ -30,7 +32,7 @@ pub struct OptionsMessageSearch {
after: Option<String>, after: Option<String>,
/// Message sort direction /// Message sort direction
/// ///
/// By default, it will be sorted by relevance. /// By default, it will be sorted by latest.
#[serde(default = "MessageSort::default")] #[serde(default = "MessageSort::default")]
sort: MessageSort, sort: MessageSort,
/// Whether to include user (and member, if server channel) objects /// Whether to include user (and member, if server channel) objects
@@ -73,10 +75,22 @@ pub async fn req(
} = options; } = options;
let messages = db 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?; .await?;
BulkMessageResponse::transform(db, &channel, messages, include_users) BulkMessageResponse::transform(db, Some(&channel), messages, include_users)
.await .await
.map(Json) .map(Json)
} }

View File

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

View File

@@ -3,6 +3,7 @@ pub use rocket::http::Status;
pub use rocket::response::Redirect; pub use rocket::response::Redirect;
use rocket::{Build, Rocket}; use rocket::{Build, Rocket};
mod admin;
mod bots; mod bots;
mod channels; mod channels;
mod customisation; mod customisation;
@@ -23,6 +24,7 @@ pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
rocket, "/".to_owned(), settings, rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()), "/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root, root::ping], "" => openapi_get_routes_spec![root::root, root::ping],
"/admin" => admin::routes(),
"/users" => users::routes(), "/users" => users::routes(),
"/bots" => bots::routes(), "/bots" => bots::routes(),
"/channels" => channels::routes(), "/channels" => channels::routes(),
@@ -111,8 +113,9 @@ fn custom_openapi_spec() -> OpenApi {
] ]
}, },
{ {
"name": "Platform Moderation", "name": "Platform Administration",
"tags": [ "tags": [
"Admin",
"User Safety" "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 revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route; use rocket::Route;
mod edit_report;
mod fetch_report;
mod fetch_reports;
mod report_content; mod report_content;
mod fetch_snapshot;
pub fn routes() -> (Vec<Route>, OpenApi) { pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![ openapi_get_routes_spec![
// Reports // 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::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::snapshot::{Snapshot, SnapshotContent};
use revolt_quark::models::{Report, User}; use revolt_quark::models::{Report, User};
use revolt_quark::{Db, Error, Result}; 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 // Collect prior context
let prior_context = db let prior_context = db
.fetch_messages( .fetch_messages(MessageQuery {
&message.channel, filter: MessageFilter {
Some(15), channel: Some(message.channel.to_string()),
Some(message.id.to_string()), ..Default::default()
None, },
None, limit: Some(15),
None, time_period: MessageTimePeriod::Absolute {
) before: Some(message.id.to_string()),
after: None,
sort: Some(MessageSort::Latest),
},
})
.await?; .await?;
// Collect leading context // Collect leading context
let leading_context = db let leading_context = db
.fetch_messages( .fetch_messages(MessageQuery {
&message.channel, filter: MessageFilter {
Some(15), channel: Some(message.channel.to_string()),
None, ..Default::default()
Some(message.id.to_string()), },
None, limit: Some(15),
None, time_period: MessageTimePeriod::Absolute {
) before: None,
after: Some(message.id.to_string()),
sort: Some(MessageSort::Oldest),
},
})
.await?; .await?;
( (
@@ -149,6 +158,8 @@ pub async fn report_content(db: &Db, user: User, data: Json<DataReportContent>)
author_id: user.id, author_id: user.id,
content: data.content, content: data.content,
additional_context: data.additional_context, additional_context: data.additional_context,
status: ReportStatus::Created {},
notes: String::new(),
}; };
db.insert_report(&report).await?; db.insert_report(&report).await?;

View File

@@ -11,20 +11,15 @@ use ulid::Ulid;
use validator::Validate; use validator::Validate;
/// # Channel Type /// # Channel Type
#[derive(Serialize, Deserialize, JsonSchema)] #[derive(Serialize, Deserialize, JsonSchema, Default)]
enum ChannelType { enum ChannelType {
/// Text Channel /// Text Channel
#[default]
Text, Text,
/// Voice Channel /// Voice Channel
Voice, Voice,
} }
impl Default for ChannelType {
fn default() -> Self {
ChannelType::Text
}
}
/// # Channel Data /// # Channel Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)] #[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataCreateChannel { pub struct DataCreateChannel {

View File

@@ -33,6 +33,10 @@ pub struct DataEditServer {
/// System message configuration /// System message configuration
system_messages: Option<SystemMessageChannels>, 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 // Whether this server is age-restricted
// nsfw: Option<bool>, // nsfw: Option<bool>,
/// Whether this server is public and should show up on [Revolt Discover](https://rvlt.gg) /// Whether this server is public and should show up on [Revolt Discover](https://rvlt.gg)
@@ -92,6 +96,11 @@ pub async fn req(
.await?; .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() { if data.categories.is_some() {
permissions permissions
.throw_permission(db, Permission::ManageChannel) .throw_permission(db, Permission::ManageChannel)
@@ -105,6 +114,7 @@ pub async fn req(
banner, banner,
categories, categories,
system_messages, system_messages,
flags,
// nsfw, // nsfw,
discoverable, discoverable,
analytics, analytics,
@@ -116,6 +126,7 @@ pub async fn req(
description, description,
categories, categories,
system_messages, system_messages,
flags,
// nsfw, // nsfw,
discoverable, discoverable,
analytics, analytics,

View File

@@ -1,6 +1,6 @@
use revolt_quark::models::user::{FieldsUser, PartialUser, User}; use revolt_quark::models::user::{FieldsUser, PartialUser, User};
use revolt_quark::models::File; 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 revolt_quark::models::user::UserStatus;
use rocket::serde::json::Json; use rocket::serde::json::Json;
@@ -24,6 +24,10 @@ pub struct UserProfileData {
/// # User Data /// # User Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)] #[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditUser { pub struct DataEditUser {
/// Attachment Id for avatar
#[validate(length(min = 1, max = 128))]
avatar: Option<String>,
/// New user status /// New user status
#[validate] #[validate]
status: Option<UserStatus>, status: Option<UserStatus>,
@@ -32,9 +36,14 @@ pub struct DataEditUser {
/// This is applied as a partial. /// This is applied as a partial.
#[validate] #[validate]
profile: Option<UserProfileData>, profile: Option<UserProfileData>,
/// Attachment Id for avatar
#[validate(length(min = 1, max = 128))] /// Bitfield of user badges
avatar: Option<String>, #[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 /// Fields to remove from user object
#[validate(length(min = 1))] #[validate(length(min = 1))]
remove: Option<Vec<FieldsUser>>, remove: Option<Vec<FieldsUser>>,
@@ -44,19 +53,36 @@ pub struct DataEditUser {
/// ///
/// Edit currently authenticated user. /// Edit currently authenticated user.
#[openapi(tag = "User Information")] #[openapi(tag = "User Information")]
#[patch("/@me", data = "<data>")] #[patch("/<target>", data = "<data>")]
pub async fn req( pub async fn req(
db: &State<Database>, db: &State<Database>,
mut user: User, mut user: User,
target: Ref,
data: Json<DataEditUser>, data: Json<DataEditUser>,
) -> Result<Json<User>> { ) -> Result<Json<User>> {
let data = data.into_inner(); let data = data.into_inner();
data.validate() data.validate()
.map_err(|error| Error::FailedValidation { error })?; .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() if data.status.is_none()
&& data.profile.is_none() && data.profile.is_none()
&& data.avatar.is_none() && data.avatar.is_none()
&& data.badges.is_none()
&& data.flags.is_none()
&& data.remove.is_none() && data.remove.is_none()
{ {
return Ok(Json(user)); 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 // 2. Apply new avatar
if let Some(avatar) = data.avatar { if let Some(avatar) = data.avatar {

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-quark" name = "revolt-quark"
version = "0.5.9" version = "0.5.17"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -29,7 +29,7 @@ default = [ "test" ]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
validator = { version = "0.14", features = ["derive"] } validator = { version = "0.14", features = ["derive"] }
iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] } iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] }
optional_struct = { git = "https://github.com/insertish/OptionalStruct", rev = "e275d2726595474632485934aa0887fa52281f70" } optional_struct = { git = "https://github.com/insertish/OptionalStruct", rev = "ee56427cee1f007839825d93d07fffd5a5e038c7" }
# Formats # Formats
bincode = "1.3.3" bincode = "1.3.3"
@@ -67,8 +67,7 @@ impl_ops = "0.1.1"
num_enum = "0.5.6" num_enum = "0.5.6"
reqwest = "0.11.10" reqwest = "0.11.10"
bitfield = "0.13.2" bitfield = "0.13.2"
once_cell = "1.13.0" once_cell = "1.17.1"
lazy_static = "1.4.0"
async-lock = "2.6.0" async-lock = "2.6.0"
lru = { version = "0.7.6", optional = true } lru = { version = "0.7.6", optional = true }

View File

@@ -557,7 +557,7 @@ impl EventV1 {
/// Publish private event /// Publish private event
pub async fn private(self, id: String) { pub async fn private(self, id: String) {
self.p(format!("{}!", id)).await; self.p(format!("{id}!")).await;
} }
/// Publish internal global event /// Publish internal global event

View File

@@ -0,0 +1,10 @@
use crate::{models::stats::Stats, AbstractStats, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractStats for DummyDb {
async fn generate_stats(&self) -> Result<Stats> {
todo!()
}
}

View File

@@ -1,4 +1,4 @@
use crate::models::message::{AppendMessage, Message, MessageSort, PartialMessage}; use crate::models::message::{AppendMessage, Message, MessageQuery, PartialMessage};
use crate::{AbstractMessage, Result}; use crate::{AbstractMessage, Result};
use super::super::DummyDb; use super::super::DummyDb;
@@ -41,28 +41,8 @@ impl AbstractMessage for DummyDb {
Ok(()) Ok(())
} }
async fn fetch_messages( async fn fetch_messages(&self, _query: MessageQuery) -> Result<Vec<Message>> {
&self, Ok(vec![])
channel: &str,
_limit: Option<i64>,
_before: Option<String>,
_after: Option<String>,
_sort: Option<MessageSort>,
_nearby: Option<String>,
) -> Result<Vec<Message>> {
Ok(vec![self.fetch_message(channel).await.unwrap()])
}
async fn search_messages(
&self,
channel: &str,
_query: &str,
_limit: Option<i64>,
_before: Option<String>,
_after: Option<String>,
_sort: MessageSort,
) -> Result<Vec<Message>> {
Ok(vec![self.fetch_message(channel).await.unwrap()])
} }
/// Add a new reaction to a message /// Add a new reaction to a message

View File

@@ -2,6 +2,7 @@ use crate::AbstractDatabase;
pub mod admin { pub mod admin {
pub mod migrations; pub mod migrations;
pub mod stats;
} }
pub mod media { pub mod media {

View File

@@ -1,3 +1,4 @@
use crate::models::report::PartialReport;
use crate::models::Report; use crate::models::Report;
use crate::{AbstractReport, Result}; use crate::{AbstractReport, Result};
@@ -9,4 +10,16 @@ impl AbstractReport for DummyDb {
info!("Insert {:?}", report); info!("Insert {:?}", report);
Ok(()) Ok(())
} }
async fn update_report(&self, _id: &str, _report: &PartialReport) -> Result<()> {
todo!()
}
async fn fetch_report(&self, _report_id: &str) -> Result<Report> {
todo!()
}
async fn fetch_reports(&self) -> Result<Vec<Report>> {
Ok(vec![])
}
} }

View File

@@ -9,4 +9,8 @@ impl AbstractSnapshot for DummyDb {
info!("Insert {:?}", snapshot); info!("Insert {:?}", snapshot);
Ok(()) Ok(())
} }
async fn fetch_snapshot(&self, _report_id: &str) -> Result<Snapshot> {
todo!()
}
} }

View File

@@ -5,13 +5,11 @@ use crate::{
Database, Error, Result, Database, Error, Result,
}; };
lazy_static! { static ALPHABET: [char; 54] = [
static ref ALPHABET: [char; 54] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'
]; ];
}
impl Invite { impl Invite {
/// Get the invite code for this invite /// Get the invite code for this invite
@@ -30,7 +28,7 @@ impl Invite {
/// Create a new invite from given information /// Create a new invite from given information
pub async fn create(db: &Database, creator: &User, target: &Channel) -> Result<Invite> { pub async fn create(db: &Database, creator: &User, target: &Channel) -> Result<Invite> {
let code = nanoid!(8, &*ALPHABET); let code = nanoid!(8, &ALPHABET);
let invite = match &target { let invite = match &target {
Channel::Group { id, .. } => Ok(Invite::Group { Channel::Group { id, .. } => Ok(Invite::Group {
code, code,

View File

@@ -385,7 +385,7 @@ impl SendableEmbed {
impl BulkMessageResponse { impl BulkMessageResponse {
pub async fn transform( pub async fn transform(
db: &Database, db: &Database,
channel: &Channel, channel: Option<&Channel>,
messages: Vec<Message>, messages: Vec<Message>,
include_users: Option<bool>, include_users: Option<bool>,
) -> Result<BulkMessageResponse> { ) -> Result<BulkMessageResponse> {
@@ -394,7 +394,8 @@ impl BulkMessageResponse {
let users = User::fetch_foreign_users(db, &user_ids).await?; let users = User::fetch_foreign_users(db, &user_ids).await?;
Ok(match channel { Ok(match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { Some(Channel::TextChannel { server, .. })
| Some(Channel::VoiceChannel { server, .. }) => {
BulkMessageResponse::MessagesAndUsers { BulkMessageResponse::MessagesAndUsers {
messages, messages,
users, users,

View File

@@ -1,4 +1,5 @@
use std::{collections::HashSet, str::FromStr}; use std::{collections::HashSet, str::FromStr};
use once_cell::sync::Lazy;
use ulid::Ulid; use ulid::Ulid;
@@ -8,13 +9,10 @@ use crate::{
Database, Result, Database, Result,
}; };
lazy_static! { static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| include_str!(crate::asset!("emojis.txt"))
/// Permissible emojis
static ref PERMISSIBLE_EMOJIS: HashSet<String> = include_str!(crate::asset!("emojis.txt"))
.split('\n') .split('\n')
.map(|x| x.into()) .map(|x| x.into())
.collect(); .collect());
}
impl Emoji { impl Emoji {
/// Get parent id /// Get parent id

View File

@@ -28,3 +28,7 @@ pub mod users {
pub mod user; pub mod user;
pub mod user_settings; pub mod user_settings;
} }
pub mod safety {
pub mod report;
}

View File

@@ -0,0 +1,9 @@
use crate::{models::report::PartialReport, models::Report, Database, Result};
impl Report {
/// Update report data
pub async fn update(&mut self, db: &Database, partial: PartialReport) -> Result<()> {
self.apply_options(partial.clone());
db.update_report(&self.id, &partial).await
}
}

View File

@@ -57,6 +57,14 @@ pub async fn create_database(db: &MongoDb) {
.await .await
.expect("Failed to create user_settings collection."); .expect("Failed to create user_settings collection.");
db.create_collection("safety_reports", None)
.await
.expect("Failed to create safety_reports collection.");
db.create_collection("safety_snapshots", None)
.await
.expect("Failed to create safety_snapshots collection.");
db.create_collection("bots", None) db.create_collection("bots", None)
.await .await
.expect("Failed to create bots collection."); .expect("Failed to create bots collection.");
@@ -103,18 +111,18 @@ pub async fn create_database(db: &MongoDb) {
}, },
"name": "content" "name": "content"
}, },
{
"key": {
"channel": 1_i32
},
"name": "channel"
},
{ {
"key": { "key": {
"channel": 1_i32, "channel": 1_i32,
"_id": 1_i32 "_id": 1_i32
}, },
"name": "channel_id_compound" "name": "channel_id_compound"
},
{
"key": {
"author": 1_i32
},
"name": "author"
} }
] ]
}, },

View File

@@ -1,4 +1,4 @@
use std::time::Duration; use std::{time::Duration, ops::BitXor};
use bson::{Bson, DateTime}; use bson::{Bson, DateTime};
use futures::StreamExt; use futures::StreamExt;
@@ -16,7 +16,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 18; pub const LATEST_REVISION: i32 = 21;
pub async fn migrate_database(db: &MongoDb) { pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations"); let migrations = db.col::<Document>("migrations");
@@ -503,13 +503,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
update.insert( update.insert(
"default_permissions", "default_permissions",
(*DEFAULT_PERMISSION_SERVER
// Remove Send Message permission if it wasn't originally granted // Remove Send Message permission if it wasn't originally granted
^ (if has_send { DEFAULT_PERMISSION_SERVER.bitxor(if has_send { 0 } else { Permission::SendMessage as u64}) as i64,
0
} else {
Permission::SendMessage as u64
})) as i64,
); );
if let Some(Bson::Document(mut roles)) = document.remove("roles") { if let Some(Bson::Document(mut roles)) = document.remove("roles") {
@@ -661,6 +656,103 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to update server members."); .expect("Failed to update server members.");
} }
if revision <= 18 {
info!("Running migration [revision 18 / 27-02-2022]: Create author index on messages. Drop plain channel index if exists.");
if db
.db()
.run_command(
doc! {
"dropIndexes": "messages",
"index": ["channel"]
},
None,
)
.await
.is_err()
{
info!("Failed to drop `messages.channel` index but this is ok since that means it's probably gone.");
}
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"author": 1_i32,
},
"name": "author"
}
]
},
None,
)
.await
.expect("Failed to create messages author index.");
}
if revision <= 19 {
info!("Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections, migrate to new model if applicable.");
// TODO: make these fail once production is migrated
if db
.db()
.create_collection("safety_reports", None)
.await
.is_err()
{
info!("Failed to create safety_reports collection but this is expected in production.");
}
if db
.db()
.create_collection("safety_snapshots", None)
.await
.is_err()
{
info!(
"Failed to create safety_snapshots collection but this is expected in production."
);
}
db.col::<Document>("safety_reports")
.update_many(
doc! {},
doc! {
"$set": {
"status": "Created"
}
},
None,
)
.await
.unwrap();
}
if revision <= 20 {
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
db.db()
.run_command(
doc! {
"createIndexes": "safety_snapshots",
"indexes": [
{
"key": {
"report_id": 1_i32
},
"name": "report_id"
}
]
},
None,
)
.await
.expect("Failed to create safety snapshot index.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`. // Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations. // Reminder to update LATEST_REVISION when adding new migrations.

View File

@@ -0,0 +1,90 @@
use std::collections::HashMap;
use bson::{from_document, Document};
use futures::StreamExt;
use crate::{
models::stats::{Index, Stats},
AbstractStats, Error, Result,
};
use super::super::MongoDb;
#[async_trait]
impl AbstractStats for MongoDb {
async fn generate_stats(&self) -> Result<Stats> {
let mut indices = HashMap::new();
let mut coll_stats = HashMap::new();
let collection_names =
self.db()
.list_collection_names(None)
.await
.map_err(|_| Error::DatabaseError {
operation: "list_collection_names",
with: "database",
})?;
for collection in collection_names {
indices.insert(
collection.to_string(),
self.col::<Document>(&collection)
.aggregate(
vec![doc! {
"$indexStats": { }
}],
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "aggregate",
with: "col",
})?
.filter_map(|s| async { s.ok() })
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|doc| from_document(doc).ok())
.collect::<Vec<Index>>(),
);
coll_stats.insert(
collection.to_string(),
self.col::<Document>(&collection)
.aggregate(
vec![doc! {
"$collStats": {
"latencyStats": {
"histograms": true
},
"storageStats": {},
"count": {},
"queryExecStats": {}
}
}],
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "aggregate",
with: "col",
})?
.filter_map(|s| async { s.ok() })
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|doc| from_document(doc).ok())
.next()
.ok_or(Error::DatabaseError {
operation: "next aggregation",
with: "col",
})?,
);
}
Ok(Stats {
indices,
coll_stats,
})
}
}

View File

@@ -2,7 +2,9 @@ use bson::{to_bson, Document};
use futures::try_join; use futures::try_join;
use mongodb::options::FindOptions; use mongodb::options::FindOptions;
use crate::models::message::{AppendMessage, Message, MessageSort, PartialMessage}; use crate::models::message::{
AppendMessage, Message, MessageQuery, MessageSort, MessageTimePeriod, PartialMessage,
};
use crate::r#impl::mongo::DocumentId; use crate::r#impl::mongo::DocumentId;
use crate::{AbstractMessage, Error, Result}; use crate::{AbstractMessage, Error, Result};
@@ -139,26 +141,60 @@ impl AbstractMessage for MongoDb {
.await .await
} }
async fn fetch_messages( async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
&self, let mut filter = doc! {};
channel: &str,
limit: Option<i64>, // 1. Apply message filters
before: Option<String>, if let Some(channel) = query.filter.channel {
after: Option<String>, filter.insert("channel", channel);
sort: Option<MessageSort>, }
nearby: Option<String>,
) -> Result<Vec<Message>> { if let Some(author) = query.filter.author {
let limit = limit.unwrap_or(50); filter.insert("author", author);
Ok(if let Some(nearby) = nearby { }
let is_search_query = if let Some(query) = query.filter.query {
filter.insert(
"$text",
doc! {
"$search": query
},
);
true
} else {
false
};
// 2. Find query limit
let limit = query.limit.unwrap_or(50);
// 3. Apply message time period
match query.time_period {
MessageTimePeriod::Relative { nearby } => {
// 3.1. Prepare filters
let mut older_message_filter = filter.clone();
let mut newer_message_filter = filter;
older_message_filter.insert(
"_id",
doc! {
"$lt": &nearby
},
);
newer_message_filter.insert(
"_id",
doc! {
"$gte": &nearby
},
);
// 3.2. Execute in both directions
let (a, b) = try_join!( let (a, b) = try_join!(
self.find_with_options::<_, Message>( self.find_with_options::<_, Message>(
COL, COL,
doc! { newer_message_filter,
"channel": channel,
"_id": {
"$gte": &nearby
}
},
FindOptions::builder() FindOptions::builder()
.limit(limit / 2 + 1) .limit(limit / 2 + 1)
.sort(doc! { .sort(doc! {
@@ -168,12 +204,7 @@ impl AbstractMessage for MongoDb {
), ),
self.find_with_options::<_, Message>( self.find_with_options::<_, Message>(
COL, COL,
doc! { older_message_filter,
"channel": channel,
"_id": {
"$lt": &nearby
}
},
FindOptions::builder() FindOptions::builder()
.limit(limit / 2) .limit(limit / 2)
.sort(doc! { .sort(doc! {
@@ -183,94 +214,56 @@ impl AbstractMessage for MongoDb {
) )
)?; )?;
[a, b].concat() Ok([a, b].concat())
} else {
let mut query = doc! { "channel": channel };
if let Some(before) = before {
query.insert("_id", doc! { "$lt": before });
} }
MessageTimePeriod::Absolute {
if let Some(after) = after { before,
query.insert("_id", doc! { "$gt": after }); after,
} sort,
} => {
let sort: i32 = if let MessageSort::Latest = sort.unwrap_or(MessageSort::Latest) { // 3.1. Apply message ID filter
-1
} else {
1
};
self.find_with_options::<_, Message>(
COL,
query,
FindOptions::builder()
.limit(limit)
.sort(doc! {
"_id": sort
})
.build(),
)
.await?
})
}
async fn search_messages(
&self,
channel: &str,
query: &str,
limit: Option<i64>,
before: Option<String>,
after: Option<String>,
sort: MessageSort,
) -> Result<Vec<Message>> {
let limit = limit.unwrap_or(50);
let mut filter = doc! {
"channel": channel,
"$text": {
"$search": query
}
};
if let Some(doc) = match (before, after) { if let Some(doc) = match (before, after) {
(Some(before), Some(after)) => Some(doc! { (Some(before), Some(after)) => Some(doc! {
"lt": before, "$lt": before,
"gt": after "$gt": after
}), }),
(Some(before), _) => Some(doc! { (Some(before), _) => Some(doc! {
"lt": before "$lt": before
}), }),
(_, Some(after)) => Some(doc! { (_, Some(after)) => Some(doc! {
"gt": after "$gt": after
}), }),
_ => None, _ => None,
} { } {
filter.insert("_id", doc); filter.insert("_id", doc);
} }
// 3.2. Execute with given message sort
self.find_with_options( self.find_with_options(
COL, COL,
filter, filter,
FindOptions::builder() FindOptions::builder()
.projection(if let MessageSort::Relevance = &sort { .limit(limit)
.sort(match sort.unwrap_or(MessageSort::Latest) {
// Sort by relevance, fallback to latest
MessageSort::Relevance => {
if is_search_query {
doc! { doc! {
"score": { "score": {
"$meta": "textScore" "$meta": "textScore"
} }
} }
} else { } else {
doc! {} doc! {
}) "_id": -1_i32
.limit(limit)
.sort(match &sort {
MessageSort::Relevance => doc! {
"score": {
"$meta": "textScore"
} }
}, }
}
// Sort by latest first
MessageSort::Latest => doc! { MessageSort::Latest => doc! {
"_id": -1_i32 "_id": -1_i32
}, },
// Sort by oldest first
MessageSort::Oldest => doc! { MessageSort::Oldest => doc! {
"_id": 1_i32 "_id": 1_i32
}, },
@@ -279,6 +272,8 @@ impl AbstractMessage for MongoDb {
) )
.await .await
} }
}
}
/// Add a new reaction to a message /// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {

View File

@@ -37,7 +37,7 @@ impl AbstractAttachment for MongoDb {
parent_type: &str, parent_type: &str,
parent_id: &str, parent_id: &str,
) -> Result<File> { ) -> Result<File> {
let key = format!("{}_id", parent_type); let key = format!("{parent_type}_id");
match self match self
.find_one::<File>( .find_one::<File>(
COL, COL,

View File

@@ -13,6 +13,7 @@ use crate::{util::manipulation::prefix_keys, AbstractDatabase, Error, Result};
pub mod admin { pub mod admin {
pub mod migrations; pub mod migrations;
pub mod stats;
} }
pub mod media { pub mod media {
@@ -90,15 +91,23 @@ impl MongoDb {
where where
O: Into<Option<FindOptions>>, O: Into<Option<FindOptions>>,
{ {
Ok(self let result = self.col::<T>(collection).find(projection, options).await;
.col::<T>(collection) Ok(if cfg!(debug_assertions) {
.find(projection, options) result.unwrap()
.await } else {
.map_err(|_| Error::DatabaseError { result.map_err(|_| Error::DatabaseError {
operation: "find", operation: "find",
with: collection, with: collection,
})? })?
.filter_map(|s| async { s.ok() }) }
.filter_map(|s| async {
if cfg!(debug_assertions) {
// Hard fail on invalid documents
Some(s.unwrap())
} else {
s.ok()
}
})
.collect::<Vec<T>>() .collect::<Vec<T>>()
.await) .await)
} }

View File

@@ -1,3 +1,4 @@
use crate::models::report::PartialReport;
use crate::models::Report; use crate::models::Report;
use crate::{AbstractReport, Result}; use crate::{AbstractReport, Result};
@@ -10,4 +11,18 @@ impl AbstractReport for MongoDb {
async fn insert_report(&self, report: &Report) -> Result<()> { async fn insert_report(&self, report: &Report) -> Result<()> {
self.insert_one(COL, report).await.map(|_| ()) self.insert_one(COL, report).await.map(|_| ())
} }
async fn update_report(&self, id: &str, report: &PartialReport) -> Result<()> {
self.update_one_by_id(COL, id, report, vec![], None)
.await
.map(|_| ())
}
async fn fetch_report(&self, report_id: &str) -> Result<Report> {
self.find_one_by_id(COL, report_id).await
}
async fn fetch_reports(&self) -> Result<Vec<Report>> {
self.find(COL, doc! {}).await
}
} }

View File

@@ -10,4 +10,14 @@ impl AbstractSnapshot for MongoDb {
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> { async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
self.insert_one(COL, snapshot).await.map(|_| ()) self.insert_one(COL, snapshot).await.map(|_| ())
} }
async fn fetch_snapshot(&self, report_id: &str) -> Result<Snapshot> {
self.find_one(
COL,
doc! {
"report_id": report_id
},
)
.await
}
} }

View File

@@ -1,6 +1,7 @@
use bson::Document; use bson::Document;
use futures::StreamExt; use futures::StreamExt;
use mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions}; use mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions};
use once_cell::sync::Lazy;
use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus, User}; use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus, User};
use crate::r#impl::mongo::IntoDocumentPath; use crate::r#impl::mongo::IntoDocumentPath;
@@ -8,16 +9,14 @@ use crate::{AbstractUser, Error, Result};
use super::super::MongoDb; use super::super::MongoDb;
lazy_static! { static FIND_USERNAME_OPTIONS: Lazy<FindOneOptions> = Lazy::new(|| FindOneOptions::builder()
static ref FIND_USERNAME_OPTIONS: FindOneOptions = FindOneOptions::builder()
.collation( .collation(
Collation::builder() Collation::builder()
.locale("en") .locale("en")
.strength(CollationStrength::Secondary) .strength(CollationStrength::Secondary)
.build() .build()
) )
.build(); .build());
}
static COL: &str = "users"; static COL: &str = "users";
@@ -265,7 +264,7 @@ impl AbstractUser for MongoDb {
[ [
{ {
"_id": target_id, "_id": target_id,
"status": format!("{:?}", relationship) "status": format!("{relationship:?}")
} }
] ]
] ]

View File

@@ -11,8 +11,6 @@ extern crate impl_ops;
#[macro_use] #[macro_use]
extern crate optional_struct; extern crate optional_struct;
#[macro_use] #[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate bitfield; extern crate bitfield;
#[macro_use] #[macro_use]
extern crate bson; extern crate bson;

View File

@@ -0,0 +1,122 @@
use std::collections::HashMap;
use iso8601_timestamp::Timestamp;
use serde::{Deserialize, Serialize};
/// Index access information
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct IndexAccess {
/// Operations since timestamp
ops: i32,
/// Timestamp at which data keeping begun
since: Timestamp,
}
/// Collection index
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct Index {
/// Index name
name: String,
/// Access information
accesses: IndexAccess,
}
/// Histogram entry
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct LatencyHistogramEntry {
/// Time
micros: i64,
/// Count
count: i64,
}
/// Collection latency stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
pub struct LatencyStats {
/// Total operations
ops: i64,
/// Timestamp at which data keeping begun
latency: i64,
/// Histogram representation of latency data
histogram: Vec<LatencyHistogramEntry>,
}
/// Collection storage stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StorageStats {
/// Uncompressed data size
size: i64,
/// Data size on disk
storage_size: i64,
/// Total size of all indexes
total_index_size: i64,
/// Sum of storage size and total index size
total_size: i64,
/// Individual index sizes
index_sizes: HashMap<String, i64>,
/// Number of documents in collection
count: i64,
/// Average size of each document
avg_obj_size: i64,
}
/// Query collection scan stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CollectionScans {
/// Number of total collection scans
total: i64,
/// Number of total collection scans not using a tailable cursor
non_tailable: i64,
}
/// Collection query execution stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QueryExecStats {
/// Stats regarding collection scans
collection_scans: CollectionScans,
}
/// Collection stats
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CollectionStats {
/// Namespace
ns: String,
/// Local time
local_time: Timestamp,
/// Latency stats
latency_stats: HashMap<String, LatencyStats>,
/// Query exec stats
query_exec_stats: QueryExecStats,
/// Number of documents in collection
count: u64,
}
/// Server Stats
#[derive(Serialize, JsonSchema, Debug)]
pub struct Stats {
/// Index usage information
pub indices: HashMap<String, Vec<Index>>,
/// Collection stats
pub coll_stats: HashMap<String, CollectionStats>,
}

View File

@@ -160,10 +160,11 @@ pub struct Message {
/// # Message Sort /// # Message Sort
/// ///
/// Sort used for retrieving messages /// Sort used for retrieving messages
#[derive(Serialize, Deserialize, JsonSchema)] #[derive(Serialize, Deserialize, JsonSchema, Debug, Default)]
#[cfg_attr(feature = "rocket_impl", derive(FromFormField))] #[cfg_attr(feature = "rocket_impl", derive(FromFormField))]
pub enum MessageSort { pub enum MessageSort {
/// Sort by the most relevant messages /// Sort by the most relevant messages
#[default]
Relevance, Relevance,
/// Sort by the newest messages first /// Sort by the newest messages first
Latest, Latest,
@@ -171,10 +172,54 @@ pub enum MessageSort {
Oldest, Oldest,
} }
impl Default for MessageSort { /// # Message Time Period
fn default() -> MessageSort { ///
MessageSort::Relevance /// Filter and sort messages by time
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum MessageTimePeriod {
Relative {
/// Message id to search around
///
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
/// It will also take half of limit rounded as the limits to each side.
/// It also fetches the message ID specified.
nearby: String,
},
Absolute {
/// Message id before which messages should be fetched
before: Option<String>,
/// Message id after which messages should be fetched
after: Option<String>,
/// Message sort direction
sort: Option<MessageSort>,
},
} }
/// # Message Filter
#[derive(Serialize, Deserialize, JsonSchema, Default)]
pub struct MessageFilter {
/// Parent channel ID
pub channel: Option<String>,
/// Message author ID
pub author: Option<String>,
/// Search query
pub query: Option<String>,
}
/// # Message Query
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct MessageQuery {
/// Maximum number of messages to fetch
///
/// For fetching nearby messages, this is \`(limit + 1)\`.
pub limit: Option<i64>,
/// Filter to apply
#[serde(flatten)]
pub filter: MessageFilter,
/// Time period to fetch
#[serde(flatten)]
pub time_period: MessageTimePeriod,
} }
/// # Bulk Message Response /// # Bulk Message Response

View File

@@ -1,10 +1,11 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Metadata associated with file /// Metadata associated with file
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum Metadata { pub enum Metadata {
/// File is just a generic uncategorised file /// File is just a generic uncategorised file
#[default]
File, File,
/// File contains textual data and should be displayed as such /// File contains textual data and should be displayed as such
Text, Text,
@@ -16,12 +17,6 @@ pub enum Metadata {
Audio, Audio,
} }
impl Default for Metadata {
fn default() -> Metadata {
Metadata::File
}
}
/// Representation of a File on Revolt /// Representation of a File on Revolt
/// Generated by Autumn /// Generated by Autumn
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)] #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Default)]

View File

@@ -1,6 +1,7 @@
mod admin { mod admin {
pub mod migrations; pub mod migrations;
pub mod simple; pub mod simple;
pub mod stats;
} }
mod media { mod media {

View File

@@ -44,6 +44,7 @@ pub enum UserReportReason {
Underage, Underage,
} }
/// The content being reported
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ReportedContent { pub enum ReportedContent {
@@ -70,8 +71,25 @@ pub enum ReportedContent {
}, },
} }
/// User-generated platform moderation report. /// Status of the report
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[serde(tag = "status")]
pub enum ReportStatus {
/// Report is waiting for triage / action
Created {},
/// Report was rejected
Rejected { rejection_reason: String },
/// Report was actioned and resolved
Resolved {},
}
/// User-generated platform moderation report.
#[derive(Serialize, Deserialize, JsonSchema, Debug, OptionalStruct, Clone)]
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
#[optional_name = "PartialReport"]
#[opt_skip_serializing_none]
pub struct Report { pub struct Report {
/// Unique Id /// Unique Id
#[serde(rename = "_id")] #[serde(rename = "_id")]
@@ -82,4 +100,11 @@ pub struct Report {
pub content: ReportedContent, pub content: ReportedContent,
/// Additional report context /// Additional report context
pub additional_context: String, pub additional_context: String,
/// Status of the report
#[opt_passthrough]
#[serde(flatten)]
pub status: ReportStatus,
/// Additional notes included on the report
#[serde(default)]
pub notes: String,
} }

View File

@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::models::{Message, Server, User}; use crate::models::{Channel, Message, Server, User};
/// Enum to map into different models /// Enum to map into different models
/// that can be saved in a snapshot /// that can be saved in a snapshot
@@ -9,11 +9,11 @@ use crate::models::{Message, Server, User};
pub enum SnapshotContent { pub enum SnapshotContent {
Message { Message {
/// Context before the message /// Context before the message
#[serde(rename = "_prior_context")] #[serde(rename = "_prior_context", default)]
prior_context: Vec<Message>, prior_context: Vec<Message>,
/// Context after the message /// Context after the message
#[serde(rename = "_leading_context")] #[serde(rename = "_leading_context", default)]
leading_context: Vec<Message>, leading_context: Vec<Message>,
/// Message /// Message
@@ -35,3 +35,20 @@ pub struct Snapshot {
/// Snapshot of content /// Snapshot of content
pub content: SnapshotContent, pub content: SnapshotContent,
} }
/// Snapshot of some content with required data to render
#[derive(Serialize, JsonSchema, Debug)]
pub struct SnapshotWithContext {
/// Snapshot itself
#[serde(flatten)]
pub snapshot: Snapshot,
/// Users involved in snapshot
#[serde(rename = "_users", skip_serializing_if = "Vec::is_empty")]
pub users: Vec<User>,
/// Channels involved in snapshot
#[serde(rename = "_channels", skip_serializing_if = "Vec::is_empty")]
pub channels: Vec<Channel>,
/// Server involved in snapshot
#[serde(rename = "_server", skip_serializing_if = "Option::is_none")]
pub server: Option<Server>,
}

View File

@@ -118,7 +118,7 @@ pub struct Server {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub banner: Option<File>, pub banner: Option<File>,
/// Enum of server flags /// Bitfield of server flags
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>, pub flags: Option<i32>,

View File

@@ -1,6 +1,7 @@
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ops; use once_cell::sync::Lazy;
use std::ops::{self, Add};
/// Permission value on Revolt /// Permission value on Revolt
/// ///
@@ -97,24 +98,18 @@ pub enum Permission {
impl_op_ex!(+ |a: &Permission, b: &Permission| -> u64 { *a as u64 | *b as u64 }); impl_op_ex!(+ |a: &Permission, b: &Permission| -> u64 { *a as u64 | *b as u64 });
impl_op_ex_commutative!(+ |a: &u64, b: &Permission| -> u64 { *a | *b as u64 }); impl_op_ex_commutative!(+ |a: &u64, b: &Permission| -> u64 { *a | *b as u64 });
lazy_static! { pub static ALLOW_IN_TIMEOUT: Lazy<u64> = Lazy::new(|| Permission::ViewChannel + Permission::ReadMessageHistory);
pub static ref ALLOW_IN_TIMEOUT: u64 = Permission::ViewChannel + Permission::ReadMessageHistory; pub static DEFAULT_PERMISSION_VIEW_ONLY: Lazy<u64> = Lazy::new(|| Permission::ViewChannel + Permission::ReadMessageHistory);
pub static ref DEFAULT_PERMISSION_VIEW_ONLY: u64 = pub static DEFAULT_PERMISSION: Lazy<u64> = Lazy::new(|| DEFAULT_PERMISSION_VIEW_ONLY.add(
Permission::ViewChannel + Permission::ReadMessageHistory; Permission::SendMessage
pub static ref DEFAULT_PERMISSION: u64 = *DEFAULT_PERMISSION_VIEW_ONLY
+ Permission::SendMessage
+ Permission::InviteOthers + Permission::InviteOthers
+ Permission::SendEmbeds + Permission::SendEmbeds
+ Permission::UploadFiles + Permission::UploadFiles
+ Permission::Connect + Permission::Connect
+ Permission::Speak; + Permission::Speak));
pub static ref DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = Permission::GrantAllSafe as u64; pub static DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = Permission::GrantAllSafe as u64;
pub static ref DEFAULT_PERMISSION_DIRECT_MESSAGE: u64 = *DEFAULT_PERMISSION pub static DEFAULT_PERMISSION_DIRECT_MESSAGE: Lazy<u64> = Lazy::new(|| DEFAULT_PERMISSION.add(Permission::ManageChannel + Permission::React));
+ Permission::ManageChannel pub static DEFAULT_PERMISSION_SERVER: Lazy<u64> = Lazy::new(|| DEFAULT_PERMISSION.add(Permission::React + Permission::ChangeNickname + Permission::ChangeAvatar));
+ Permission::React;
pub static ref DEFAULT_PERMISSION_SERVER: u64 =
*DEFAULT_PERMISSION + Permission::React + Permission::ChangeNickname + Permission::ChangeAvatar;
}
bitfield! { bitfield! {
#[derive(Default)] #[derive(Default)]

View File

@@ -113,7 +113,7 @@ async fn calculate_channel_permission(
let value: PermissionValue = match channel { let value: PermissionValue = match channel {
Channel::SavedMessages { user, .. } => { Channel::SavedMessages { user, .. } => {
if user == &data.perspective.id { if user == &data.perspective.id {
(*DEFAULT_PERMISSION_SAVED_MESSAGES).into() DEFAULT_PERMISSION_SAVED_MESSAGES.into()
} else { } else {
0_u64.into() 0_u64.into()
} }

View File

@@ -51,6 +51,10 @@ pub fn get_relationship(a: &User, b: &str) -> RelationshipStatus {
async fn calculate_permission(data: &mut PermissionCalculator<'_>, db: &crate::Database) -> u32 { async fn calculate_permission(data: &mut PermissionCalculator<'_>, db: &crate::Database) -> u32 {
let user = data.user.get().unwrap(); let user = data.user.get().unwrap();
if data.perspective.privileged {
return u32::MAX;
}
if data.perspective.id == user.id { if data.perspective.id == user.id {
return u32::MAX; return u32::MAX;
} }

View File

@@ -1,14 +1,14 @@
use std::env; use std::env;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use once_cell::sync::Lazy;
lazy_static! { pub static REGION_ID: Lazy<u16> = Lazy::new(|| env::var("REGION_ID")
pub static ref REGION_ID: u16 = env::var("REGION_ID")
.unwrap_or_else(|_| "0".to_string()) .unwrap_or_else(|_| "0".to_string())
.parse() .parse()
.unwrap(); .unwrap());
pub static ref REGION_KEY: String = format!("region{}", &*REGION_ID);
} pub static REGION_KEY: Lazy<String> = Lazy::new(|| format!("region{}", &*REGION_ID));
/// Compact presence information for a user /// Compact presence information for a user
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View File

@@ -2,8 +2,8 @@
use crate::Database; use crate::Database;
use deadqueue::limited::Queue; use deadqueue::limited::Queue;
use mongodb::bson::doc;
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, time::Duration};
use once_cell::sync::Lazy;
use super::DelayedTask; use super::DelayedTask;
@@ -38,9 +38,7 @@ struct Task {
event: AckEvent, event: AckEvent,
} }
lazy_static! { static Q: Lazy<Queue<Data>> = Lazy::new(|| Queue::new(10_000));
static ref Q: Queue<Data> = Queue::new(10_000);
}
/// Queue a new task for a worker /// Queue a new task for a worker
pub async fn queue(channel: String, user: String, event: AckEvent) { pub async fn queue(channel: String, user: String, event: AckEvent) {

View File

@@ -2,8 +2,8 @@
use crate::{models::channel::PartialChannel, Database}; use crate::{models::channel::PartialChannel, Database};
use deadqueue::limited::Queue; use deadqueue::limited::Queue;
use mongodb::bson::doc;
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, time::Duration};
use once_cell::sync::Lazy;
use super::DelayedTask; use super::DelayedTask;
@@ -26,9 +26,7 @@ struct Task {
is_dm: bool, is_dm: bool,
} }
lazy_static! { static Q: Lazy<Queue<Data>> = Lazy::new(|| Queue::new(10_000));
static ref Q: Queue<Data> = Queue::new(10_000);
}
/// Queue a new task for a worker /// Queue a new task for a worker
pub async fn queue(channel: String, id: String, is_dm: bool) { pub async fn queue(channel: String, id: String, is_dm: bool) {

View File

@@ -9,6 +9,7 @@ use async_lock::Semaphore;
use async_std::task::spawn; use async_std::task::spawn;
use deadqueue::limited::Queue; use deadqueue::limited::Queue;
use std::sync::Arc; use std::sync::Arc;
use once_cell::sync::Lazy;
/// Task information /// Task information
#[derive(Debug)] #[derive(Debug)]
@@ -21,9 +22,8 @@ struct EmbedTask {
content: String, content: String,
} }
lazy_static! { static Q: Lazy<Queue<EmbedTask>> = Lazy::new(|| Queue::new(10_000));
static ref Q: Queue<EmbedTask> = Queue::new(10_000);
}
/// Queue a new task for a worker /// Queue a new task for a worker
pub async fn queue(channel: String, id: String, content: String) { pub async fn queue(channel: String, id: String, content: String) {

View File

@@ -1,8 +1,8 @@
use crate::bson::doc;
use crate::util::variables::delta::VAPID_PRIVATE_KEY; use crate::util::variables::delta::VAPID_PRIVATE_KEY;
use authifier::Database; use authifier::Database;
use deadqueue::limited::Queue; use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use web_push::{ use web_push::{
ContentEncoding, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, WebPushClient, ContentEncoding, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, WebPushClient,
WebPushMessageBuilder, WebPushMessageBuilder,
@@ -17,9 +17,8 @@ struct PushTask {
payload: String, payload: String,
} }
lazy_static! { static Q: Lazy<Queue<PushTask>> = Lazy::new(|| Queue::new(10_000));
static ref Q: Queue<PushTask> = Queue::new(10_000);
}
/// Queue a new task for a worker /// Queue a new task for a worker
pub async fn queue(recipients: Vec<String>, payload: String) { pub async fn queue(recipients: Vec<String>, payload: String) {

View File

@@ -0,0 +1,6 @@
use crate::{models::stats::Stats, Result};
#[async_trait]
pub trait AbstractStats: Sync + Send {
async fn generate_stats(&self) -> Result<Stats>;
}

View File

@@ -1,4 +1,4 @@
use crate::models::message::{AppendMessage, Message, MessageSort, PartialMessage}; use crate::models::message::{AppendMessage, Message, MessageQuery, PartialMessage};
use crate::Result; use crate::Result;
#[async_trait] #[async_trait]
@@ -21,27 +21,8 @@ pub trait AbstractMessage: Sync + Send {
/// Delete messages from a channel by their ids and corresponding channel id /// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()>; async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()>;
/// Fetch multiple messages /// Fetch multiple messages by given query
async fn fetch_messages( async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
&self,
channel: &str,
limit: Option<i64>,
before: Option<String>,
after: Option<String>,
sort: Option<MessageSort>,
nearby: Option<String>,
) -> Result<Vec<Message>>;
/// Search for messages
async fn search_messages(
&self,
channel: &str,
query: &str,
limit: Option<i64>,
before: Option<String>,
after: Option<String>,
sort: MessageSort,
) -> Result<Vec<Message>>;
/// Add a new reaction to a message /// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>; async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;

View File

@@ -1,5 +1,6 @@
mod admin { mod admin {
pub mod migrations; pub mod migrations;
pub mod stats;
} }
mod media { mod media {
@@ -33,6 +34,7 @@ mod safety {
} }
pub use admin::migrations::AbstractMigrations; pub use admin::migrations::AbstractMigrations;
pub use admin::stats::AbstractStats;
pub use media::attachment::AbstractAttachment; pub use media::attachment::AbstractAttachment;
pub use media::emoji::AbstractEmoji; pub use media::emoji::AbstractEmoji;
@@ -58,6 +60,7 @@ pub trait AbstractDatabase:
Sync Sync
+ Send + Send
+ AbstractMigrations + AbstractMigrations
+ AbstractStats
+ AbstractAttachment + AbstractAttachment
+ AbstractEmoji + AbstractEmoji
+ AbstractChannel + AbstractChannel

View File

@@ -1,3 +1,4 @@
use crate::models::report::PartialReport;
use crate::models::Report; use crate::models::Report;
use crate::Result; use crate::Result;
@@ -5,4 +6,13 @@ use crate::Result;
pub trait AbstractReport: Sync + Send { pub trait AbstractReport: Sync + Send {
/// Insert a new report into the database /// Insert a new report into the database
async fn insert_report(&self, report: &Report) -> Result<()>; async fn insert_report(&self, report: &Report) -> Result<()>;
/// Update a given report with new information
async fn update_report(&self, id: &str, message: &PartialReport) -> Result<()>;
/// Fetch report
async fn fetch_report(&self, report_id: &str) -> Result<Report>;
/// Fetch reports
async fn fetch_reports(&self) -> Result<Vec<Report>>;
} }

View File

@@ -5,4 +5,7 @@ use crate::Result;
pub trait AbstractSnapshot: Sync + Send { 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
async fn fetch_snapshot(&self, report_id: &str) -> Result<Snapshot>;
} }

View File

@@ -5,6 +5,7 @@ use linkify::{LinkFinder, LinkKind};
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashSet, sync::Arc}; use std::{collections::HashSet, sync::Arc};
use once_cell::sync::Lazy;
use crate::{models::attachment::File, Error, Result}; use crate::{models::attachment::File, Error, Result};
@@ -175,6 +176,9 @@ pub enum Embed {
None, None,
} }
static RE_CODE: Lazy<Regex> = Lazy::new(|| Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap());
static RE_IGNORED: Lazy<Regex> = Lazy::new(|| Regex::new("(<http.+>)").unwrap());
impl Embed { impl Embed {
/// Generate embeds from given content /// Generate embeds from given content
pub async fn generate( pub async fn generate(
@@ -183,10 +187,7 @@ impl Embed {
max_embeds: usize, max_embeds: usize,
semaphore: Arc<Semaphore>, semaphore: Arc<Semaphore>,
) -> Result<Vec<Embed>> { ) -> Result<Vec<Embed>> {
lazy_static! {
static ref RE_CODE: Regex = Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap();
static ref RE_IGNORED: Regex = Regex::new("(<http.+>)").unwrap();
}
// Ignore code blocks. // Ignore code blocks.
let content = RE_CODE.replace_all(&content, ""); let content = RE_CODE.replace_all(&content, "");

View File

@@ -83,6 +83,11 @@ impl Ref {
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> { pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await db.fetch_webhook(&self.id).await
} }
/// Fetch report from Ref
pub async fn as_report(&self, db: &Database) -> Result<Report> {
db.fetch_report(&self.id).await
}
} }
impl<'r> FromParam<'r> for Ref { impl<'r> FromParam<'r> for Ref {

View File

@@ -75,6 +75,7 @@ pub enum Error {
permission: UserPermission, permission: UserPermission,
}, },
NotElevated, NotElevated,
NotPrivileged,
CannotGiveMissingPermissions, CannotGiveMissingPermissions,
NotOwner, NotOwner,
@@ -174,6 +175,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::MissingPermission { .. } => Status::Forbidden, Error::MissingPermission { .. } => Status::Forbidden,
Error::MissingUserPermission { .. } => Status::Forbidden, Error::MissingUserPermission { .. } => Status::Forbidden,
Error::NotElevated => Status::Forbidden, Error::NotElevated => Status::Forbidden,
Error::NotPrivileged => Status::Forbidden,
Error::CannotGiveMissingPermissions => Status::Forbidden, Error::CannotGiveMissingPermissions => Status::Forbidden,
Error::NotOwner => Status::Forbidden, Error::NotOwner => Status::Forbidden,

View File

@@ -1,73 +1,50 @@
use std::env; use std::env;
use once_cell::sync::Lazy;
lazy_static! {
// Application Settings // Application Settings
pub static ref PUBLIC_URL: String = pub static PUBLIC_URL: Lazy<String> = Lazy::new(|| env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable."));
env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable."); pub static APP_URL: Lazy<String> = Lazy::new(|| env::var("REVOLT_APP_URL").expect("Missing REVOLT_APP_URL environment variable."));
pub static ref APP_URL: String = pub static EXTERNAL_WS_URL: Lazy<String> = Lazy::new(|| env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable."));
env::var("REVOLT_APP_URL").expect("Missing REVOLT_APP_URL environment variable.");
pub static ref EXTERNAL_WS_URL: String =
env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable.");
pub static ref AUTUMN_URL: String = pub static AUTUMN_URL: Lazy<String> = Lazy::new(|| env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()));
env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()); pub static JANUARY_URL: Lazy<String> = Lazy::new(|| env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()));
pub static ref JANUARY_URL: String = pub static JANUARY_CONCURRENT_CONNECTIONS: Lazy<usize> = Lazy::new(|| env::var("JANUARY_CONCURRENT_CONNECTIONS").map_or(50, |v| v.parse().unwrap()));
env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()); pub static VOSO_URL: Lazy<String> = Lazy::new(|| env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string()));
pub static ref JANUARY_CONCURRENT_CONNECTIONS: usize = pub static VOSO_WS_HOST: Lazy<String> = Lazy::new(|| env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string()));
env::var("JANUARY_CONCURRENT_CONNECTIONS").map_or(50, |v| v.parse().unwrap()); pub static VOSO_MANAGE_TOKEN: Lazy<String> = Lazy::new(|| env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string()));
pub static ref VOSO_URL: String =
env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
pub static ref VOSO_WS_HOST: String =
env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string());
pub static ref VOSO_MANAGE_TOKEN: String =
env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string());
pub static ref HCAPTCHA_KEY: String = pub static HCAPTCHA_KEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string()));
env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string()); pub static HCAPTCHA_SITEKEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string()));
pub static ref HCAPTCHA_SITEKEY: String = pub static VAPID_PRIVATE_KEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable."));
env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string()); pub static VAPID_PUBLIC_KEY: Lazy<String> = Lazy::new(|| env::var("REVOLT_VAPID_PUBLIC_KEY").expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable."));
pub static ref VAPID_PRIVATE_KEY: String = pub static AUTHIFIER_SHIELD_KEY: Lazy<Option<String>> = Lazy::new(|| env::var("REVOLT_AUTHIFIER_SHIELD_KEY").ok());
env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable.");
pub static ref VAPID_PUBLIC_KEY: String =
env::var("REVOLT_VAPID_PUBLIC_KEY").expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable.");
pub static ref AUTHIFIER_SHIELD_KEY: Option<String> =
env::var("REVOLT_AUTHIFIER_SHIELD_KEY").ok();
// Application Flags // Application Flags
pub static ref INVITE_ONLY: bool = env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1"); pub static INVITE_ONLY: Lazy<bool> = Lazy::new(|| env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1"));
pub static ref USE_EMAIL: bool = env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or( pub static USE_EMAIL: Lazy<bool> = Lazy::new(|| env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or(
env::var("REVOLT_SMTP_HOST").is_ok() env::var("REVOLT_SMTP_HOST").is_ok()
&& env::var("REVOLT_SMTP_USERNAME").is_ok() && env::var("REVOLT_SMTP_USERNAME").is_ok()
&& env::var("REVOLT_SMTP_PASSWORD").is_ok() && env::var("REVOLT_SMTP_PASSWORD").is_ok()
&& env::var("REVOLT_SMTP_FROM").is_ok(), && env::var("REVOLT_SMTP_FROM").is_ok(),
|v| v == *"1" |v| v == *"1"
); ));
pub static ref USE_HCAPTCHA: bool = env::var("REVOLT_HCAPTCHA_KEY").is_ok(); pub static USE_HCAPTCHA: Lazy<bool> = Lazy::new(|| env::var("REVOLT_HCAPTCHA_KEY").is_ok());
pub static ref USE_AUTUMN: bool = env::var("AUTUMN_PUBLIC_URL").is_ok(); pub static USE_AUTUMN: Lazy<bool> = Lazy::new(|| env::var("AUTUMN_PUBLIC_URL").is_ok());
pub static ref USE_JANUARY: bool = env::var("JANUARY_PUBLIC_URL").is_ok(); pub static USE_JANUARY: Lazy<bool> = Lazy::new(|| env::var("JANUARY_PUBLIC_URL").is_ok());
pub static ref USE_VOSO: bool = env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok(); pub static USE_VOSO: Lazy<bool> = Lazy::new(|| env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok());
// SMTP Settings // SMTP Settings
pub static ref SMTP_HOST: String = pub static SMTP_HOST: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string()));
env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string()); pub static SMTP_USERNAME: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_USERNAME").unwrap_or_else(|_| "".to_string()));
pub static ref SMTP_USERNAME: String = pub static SMTP_PASSWORD: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_PASSWORD").unwrap_or_else(|_| "".to_string()));
env::var("REVOLT_SMTP_USERNAME").unwrap_or_else(|_| "".to_string()); pub static SMTP_FROM: Lazy<String> = Lazy::new(|| env::var("REVOLT_SMTP_FROM").unwrap_or_else(|_| "".to_string()));
pub static ref SMTP_PASSWORD: String =
env::var("REVOLT_SMTP_PASSWORD").unwrap_or_else(|_| "".to_string());
pub static ref SMTP_FROM: String = env::var("REVOLT_SMTP_FROM").unwrap_or_else(|_| "".to_string());
// Application Logic Settings // Application Logic Settings
pub static ref MAX_GROUP_SIZE: usize = pub static MAX_GROUP_SIZE: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap());
env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap(); pub static MAX_BOT_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_BOT_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap());
pub static ref MAX_BOT_COUNT: usize = pub static MAX_EMBED_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_EMBED_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap());
env::var("REVOLT_MAX_BOT_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap(); pub static MAX_SERVER_COUNT: Lazy<usize> = Lazy::new(|| env::var("REVOLT_MAX_SERVER_COUNT").unwrap_or_else(|_| "100".to_string()).parse().unwrap());
pub static ref MAX_EMBED_COUNT: usize = pub static EARLY_ADOPTER_BADGE: Lazy<i64> = Lazy::new(|| env::var("REVOLT_EARLY_ADOPTER_BADGE").unwrap_or_else(|_| "0".to_string()).parse().unwrap());
env::var("REVOLT_MAX_EMBED_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap();
pub static ref MAX_SERVER_COUNT: usize =
env::var("REVOLT_MAX_SERVER_COUNT").unwrap_or_else(|_| "100".to_string()).parse().unwrap();
pub static ref EARLY_ADOPTER_BADGE: i64 =
env::var("REVOLT_EARLY_ADOPTER_BADGE").unwrap_or_else(|_| "0".to_string()).parse().unwrap();
}
pub fn preflight_checks() { pub fn preflight_checks() {
format!("url = {}", *APP_URL); format!("url = {}", *APP_URL);

View File

@@ -9,6 +9,7 @@ use rocket::request::{FromRequest, Outcome};
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec}; use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
use once_cell::sync::Lazy;
#[derive(Validate, Serialize, Deserialize)] #[derive(Validate, Serialize, Deserialize)]
pub struct IdempotencyKey { pub struct IdempotencyKey {
@@ -16,9 +17,7 @@ pub struct IdempotencyKey {
key: String, key: String,
} }
lazy_static! { static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> = Lazy::new(|| Mutex::new(lru::LruCache::new(100)));
static ref TOKEN_CACHE: Mutex<lru::LruCache<String, ()>> = Mutex::new(lru::LruCache::new(100));
}
impl IdempotencyKey { impl IdempotencyKey {
// Backwards compatibility. // Backwards compatibility.

View File

@@ -22,17 +22,16 @@ use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use serde::Serialize; use serde::Serialize;
use dashmap::DashMap; use dashmap::DashMap;
use once_cell::sync::Lazy;
/// Ratelimit Bucket /// Ratelimit Bucket
#[derive(Clone, Copy)] #[derive(Clone, Copy, Debug)]
struct Entry { struct Entry {
used: u8, used: u8,
reset: u128, reset: u128,
} }
lazy_static! { static MAP: Lazy<DashMap<u64, Entry>> = Lazy::new(DashMap::new);
static ref MAP: DashMap<u64, Entry> = DashMap::new();
}
/// Get the current time from Unix Epoch as a Duration /// Get the current time from Unix Epoch as a Duration
fn now() -> Duration { fn now() -> Duration {
@@ -51,7 +50,7 @@ impl Entry {
} }
/// Deduct one unit from the bucket and save /// Deduct one unit from the bucket and save
pub fn deduct(mut self) { pub fn deduct(&mut self) {
let current_time = now().as_millis(); let current_time = now().as_millis();
if current_time > self.reset { if current_time > self.reset {
self.used = 1; self.used = 1;
@@ -193,7 +192,7 @@ impl Ratelimiter {
let key = key.finish(); let key = key.finish();
let limit = resolve_bucket_limit(bucket); let limit = resolve_bucket_limit(bucket);
let entry = Entry::from(key); let mut entry = Entry::from(key);
let remaining = entry.get_remaining(limit); let remaining = entry.get_remaining(limit);
if remaining > 0 { if remaining > 0 {