Compare commits

...
14 Commits
60 changed files with 823 additions and 1150 deletions

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
# MongoDB URI
MONGODB=mongodb://localhost
# URL to where the Revolt app is publicly accessible
REVOLT_APP_URL=http://local.revolt.chat:5000
# URL to where the API is publicly accessible
REVOLT_PUBLIC_URL=http://local.revolt.chat:8000
VITE_API_URL=http://local.revolt.chat:8000
# URL to where the WebSocket server is publicly accessible
REVOLT_EXTERNAL_WS_URL=ws://local.revolt.chat:9000
# URL to where Autumn is publicly available
AUTUMN_PUBLIC_URL=http://local.revolt.chat:3000
# URL to where January is publicly available
JANUARY_PUBLIC_URL=http://local.revolt.chat:7000
# URL to where Vortex is publicly available
# VOSO_PUBLIC_URL=https://voso.revolt.chat
##
## hCaptcha Settings
##
# If you are sure that you don't want to use hCaptcha, set to 1.
REVOLT_UNSAFE_NO_CAPTCHA=1
# hCaptcha API key
# REVOLT_HCAPTCHA_KEY=0x0000000000000000000000000000000000000000
# hCaptcha site key
# REVOLT_HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
##
## Email Settings
##
# If you are sure that you don't want to use email verification, set to 1.
REVOLT_UNSAFE_NO_EMAIL=1
# SMTP host
# REVOLT_SMTP_HOST=smtp.example.com
# SMTP username
# REVOLT_SMTP_USERNAME=noreply@example.com
# SMTP password
# REVOLT_SMTP_PASSWORD=CHANGEME
# SMTP From header
# REVOLT_SMTP_FROM=Revolt <noreply@example.com>
##
## Application Settings
##
# Whether to only allow users to sign up if they have an invite code
REVOLT_INVITE_ONLY=0
# Maximum number of people that can be in a group chat
REVOLT_MAX_GROUP_SIZE=150
# VAPID keys for push notifications
# Generate using this guide: https://gitlab.insrt.uk/revolt/delta/-/wikis/vapid
# --> Please replace these keys before going into production! <--
REVOLT_VAPID_PRIVATE_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw=
##
## Vortex configuration
##
# VOSO_MANAGE_TOKEN=CHANGEME
+1
View File
@@ -1,2 +1,3 @@
Rocket.toml
target
.data
Generated
+251 -793
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -14,7 +14,7 @@ mod websocket;
#[async_std::main]
async fn main() {
// Configure requirements for Bonfire.
let _guard = revolt_quark::setup_logging();
revolt_quark::configure!();
database::connect().await;
// Clean up the current region information.
+5 -8
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.5.3-6"
version = "0.5.3-7"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -18,7 +18,6 @@ linkify = "0.6.0"
once_cell = "1.4.1"
env_logger = "0.7.1"
lazy_static = "1.4.0"
ctrlc = { version = "3.0", features = ["termination"] }
# Lang. Utilities
regex = "1"
@@ -44,7 +43,6 @@ async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"]
# internal util
lettre = "0.10.0-alpha.4"
rauth = { git = "https://github.com/insertish/rauth", rev = "001a9698c56cea79e69e4ae71d7bc2cb48aec1a6" }
# redis
redis = { version = "0.21.2", features = ["async-std-comp"] }
@@ -52,15 +50,14 @@ mobc = { version = "0.7.3" }
mobc-redis = { version = "0.7.0", default-features = false, features = ["async-std-comp"] }
# web
rocket_empty = { git = "https://github.com/insertish/rocket_empty", branch = "rc1" }
rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] }
mongodb = { version = "1.2.2", features = ["async-std-runtime"], default-features = false }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" }
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] }
rocket_empty = { git = "https://github.com/insertish/rocket_empty", branch = "master" }
rocket_rauth = { git = "https://github.com/insertish/rauth", rev = "d68558d3211268068a38aa5012ac284e679609c3" }
# spec generation
schemars = "0.8.8"
# rocket_okapi = "0.8.0-rc.1"
rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "dcf0df115596ee07a587a7a543cddf3d7944645b", features = [ "swagger" ] }
rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "a1048d0c8cd771e424ec97d33d825c32e06aa120", features = [ "swagger" ] }
# quark
revolt-quark = { path = "../quark" }
+21 -119
View File
@@ -6,144 +6,46 @@ extern crate rocket_okapi;
extern crate serde_json;
#[macro_use]
extern crate lazy_static;
extern crate ctrlc;
pub mod routes;
pub mod util;
pub mod version;
use log::info;
use rauth::{
config::{Captcha, Config, EmailVerification, SMTPSettings, Template, Templates},
logic::Auth,
};
use revolt_quark::variables::delta::{
APP_URL, HCAPTCHA_KEY, INVITE_ONLY, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_USERNAME,
USE_EMAIL, USE_HCAPTCHA,
};
use revolt_quark::rauth::RAuth;
use revolt_quark::DatabaseInfo;
use rocket_cors::AllowedOrigins;
use std::str::FromStr;
#[async_std::main]
async fn main() {
let _guard = revolt_quark::setup_logging();
info!(
"Starting Revolt server [version {}].",
crate::version::VERSION
);
#[launch]
async fn rocket() -> _ {
// Configure logging and environment
revolt_quark::configure!();
// Ensure environment variables are present
revolt_quark::variables::delta::preflight_checks();
#[cfg(debug_assertions)]
ctrlc::set_handler(move || {
// Force ungraceful exit to avoid hang.
std::process::exit(0);
})
.expect("Error setting Ctrl-C handler");
let cors = rocket_cors::CorsOptions {
allowed_origins: AllowedOrigins::All,
allowed_methods: [
"Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch",
]
.iter()
.map(|s| FromStr::from_str(s).unwrap())
.collect(),
..Default::default()
}
.to_cors()
.expect("Failed to create CORS.");
let mut config = Config {
email_verification: if *USE_EMAIL {
EmailVerification::Enabled {
smtp: SMTPSettings {
from: (*SMTP_FROM).to_string(),
host: (*SMTP_HOST).to_string(),
username: (*SMTP_USERNAME).to_string(),
password: (*SMTP_PASSWORD).to_string(),
reply_to: Some("support@revolt.chat".into()),
port: None,
use_tls: None,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!(crate::asset!("templates/verify.txt")).into(),
url: format!("{}/login/verify/", *APP_URL),
html: None,
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!(crate::asset!("templates/reset.txt")).into(),
url: format!("{}/login/reset/", *APP_URL),
html: None,
},
welcome: None,
},
}
} else {
EmailVerification::Disabled
},
..Default::default()
};
if *INVITE_ONLY {
config.invite_only = true;
}
if *USE_HCAPTCHA {
config.captcha = Captcha::HCaptcha {
secret: HCAPTCHA_KEY.clone(),
};
}
// Setup database
let db = DatabaseInfo::Auto.connect().await.unwrap();
db.migrate_database().await.unwrap();
// This is entirely temporary code until rauth is migrated to quark.
// (and / or otherwise gets updated to MongoDB v2 driver)
let mongo_db = mongodb::Client::with_uri_str(
&std::env::var("MONGODB").unwrap_or_else(|_| "mongodb://localhost".to_string()),
)
.await
.expect("Failed to init db connection.");
rauth::entities::sync_models(&mongo_db.database("revolt")).await;
// Setup rAuth
let rauth = RAuth {
database: db.clone().into(),
config: revolt_quark::util::rauth::config(),
};
// Launch background task workers.
async_std::task::spawn(revolt_quark::tasks::start_workers(db.clone()));
let auth = Auth::new(mongo_db.database("revolt"), config);
// Configure CORS
let cors = revolt_quark::web::cors::new();
// Configure Rocket
let rocket = rocket::build();
routes::mount(rocket)
.mount("/", rocket_cors::catch_all_options_routes())
.mount("/", util::ratelimiter::routes())
.mount(
"/swagger/",
rocket_okapi::swagger_ui::make_swagger_ui(&rocket_okapi::swagger_ui::SwaggerUIConfig {
url: "../openapi.json".to_owned(),
..Default::default()
}),
)
.manage(auth)
.mount("/", revolt_quark::web::cors::catch_all_options_routes())
.mount("/", revolt_quark::web::ratelimiter::routes())
.mount("/swagger/", revolt_quark::web::swagger::routes())
.manage(rauth)
.manage(db)
.manage(cors.clone())
.attach(util::ratelimiter::RatelimitFairing)
.attach(revolt_quark::web::ratelimiter::RatelimitFairing)
.attach(cors)
.launch()
.await
.unwrap();
}
/// Resolve asset
macro_rules! asset {
($path:literal) => {
concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $path)
};
}
pub(crate) use asset;
@@ -5,7 +5,9 @@ use revolt_quark::{
message::{Masquerade, Reply, SendableEmbed},
Message, User,
},
perms, Db, Error, Permission, Ref, Result,
perms,
web::idempotency::IdempotencyKey,
Db, Error, Permission, Ref, Result,
};
use regex::Regex;
@@ -14,8 +16,6 @@ use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
use crate::util::idempotency::IdempotencyKey;
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataMessageSend {
/// Unique token to prevent duplicate message sending
@@ -134,7 +134,19 @@ pub async fn message_send(
.replace(replies.into_iter().collect::<Vec<String>>());
}
// 4. Add attachments to message.
// 4. Process included embeds.
let mut embeds = vec![];
if let Some(sendable_embeds) = data.embeds {
for sendable_embed in sendable_embeds {
embeds.push(sendable_embed.into_embed(db, message_id.clone()).await?)
}
}
if !embeds.is_empty() {
message.embeds.replace(embeds);
}
// 5. Add attachments to message.
let mut attachments = vec![];
if let Some(ids) = &data.attachments {
if !ids.is_empty() {
@@ -160,18 +172,6 @@ pub async fn message_send(
message.attachments.replace(attachments);
}
// 5. Process included embeds.
let mut embeds = vec![];
if let Some(sendable_embeds) = data.embeds {
for sendable_embed in sendable_embeds {
embeds.push(sendable_embed.into_embed(db, message_id.clone()).await?)
}
}
if !embeds.is_empty() {
message.embeds.replace(embeds);
}
// 6. Set content
message.content = data.content;
+12 -5
View File
@@ -25,8 +25,9 @@ pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/auth/account" => rauth::web::account::routes(),
"/auth/session" => rauth::web::session::routes(),
"/auth/account" => rocket_rauth::routes::account::routes(),
"/auth/session" => rocket_rauth::routes::session::routes(),
"/auth/mfa" => rocket_rauth::routes::mfa::routes(),
"/onboard" => onboard::routes(),
"/push" => push::routes(),
"/sync" => sync::routes(),
@@ -100,7 +101,8 @@ fn custom_openapi_spec() -> OpenApi {
"tags": [
"Account",
"Session",
"Onboarding"
"Onboarding",
"MFA"
]
},
{
@@ -117,7 +119,7 @@ fn custom_openapi_spec() -> OpenApi {
openapi: OpenApi::default_version(),
info: Info {
title: "Revolt API".to_owned(),
description: Some("User-first privacy focused chat platform.".to_owned()),
description: Some("Open source user-first chat platform.".to_owned()),
terms_of_service: Some("https://revolt.chat/terms".to_owned()),
contact: Some(Contact {
name: Some("Revolt Support".to_owned()),
@@ -130,7 +132,7 @@ fn custom_openapi_spec() -> OpenApi {
url: Some("https://github.com/revoltchat/delta/blob/master/LICENSE".to_owned()),
..Default::default()
}),
version: "0.5.3-rc.1".to_owned(),
version: env!("CARGO_PKG_VERSION").to_string(),
..Default::default()
},
servers: vec![
@@ -242,6 +244,11 @@ fn custom_openapi_spec() -> OpenApi {
description: Some("Create and manage sessions".to_owned()),
..Default::default()
},
Tag {
name: "MFA".to_owned(),
description: Some("Multi-factor Authentication".to_owned()),
..Default::default()
},
Tag {
name: "Onboarding".to_owned(),
description: Some(
+1 -2
View File
@@ -1,7 +1,6 @@
use crate::util::regex::RE_USERNAME;
use revolt_quark::{models::User, Database, EmptyResponse, Error, Result};
use revolt_quark::{models::User, rauth::models::Session, Database, EmptyResponse, Error, Result};
use rauth::entities::Session;
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use validator::Validate;
+1 -2
View File
@@ -1,5 +1,4 @@
use rauth::entities::Session;
use revolt_quark::models::User;
use revolt_quark::{models::User, rauth::models::Session};
use rocket::serde::json::Json;
use serde::Serialize;
+9 -7
View File
@@ -1,9 +1,11 @@
use revolt_quark::{EmptyResponse, Error, Result};
use rauth::{
entities::{Model, Session, WebPushSubscription},
logic::Auth,
use revolt_quark::{
rauth::{
models::{Session, WebPushSubscription},
RAuth,
},
EmptyResponse, Error, Result,
};
use rocket::{serde::json::Json, State};
/// # Push Subscribe
@@ -14,13 +16,13 @@ use rocket::{serde::json::Json, State};
#[openapi(tag = "Web Push")]
#[post("/subscribe", data = "<data>")]
pub async fn req(
auth: &State<Auth>,
rauth: &State<RAuth>,
mut session: Session,
data: Json<WebPushSubscription>,
) -> Result<EmptyResponse> {
session.subscription = Some(data.into_inner());
session
.save(&auth.db, None)
.save(&rauth)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
+6 -7
View File
@@ -1,9 +1,8 @@
use revolt_quark::{EmptyResponse, Error, Result};
use rauth::{
entities::{Model, Session},
logic::Auth,
use revolt_quark::{
rauth::{models::Session, RAuth},
EmptyResponse, Error, Result,
};
use rocket::State;
/// # Unsubscribe
@@ -11,10 +10,10 @@ use rocket::State;
/// Remove the Web Push subscription associated with the current session.
#[openapi(tag = "Web Push")]
#[post("/unsubscribe")]
pub async fn req(auth: &State<Auth>, mut session: Session) -> Result<EmptyResponse> {
pub async fn req(rauth: &State<RAuth>, mut session: Session) -> Result<EmptyResponse> {
session.subscription = None;
session
.save(&auth.db, None)
.save(&rauth)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
+1 -1
View File
@@ -76,7 +76,7 @@ pub struct RevoltConfig {
#[get("/")]
pub async fn root() -> Result<Json<RevoltConfig>> {
Ok(Json(RevoltConfig {
revolt: crate::version::VERSION.to_string(),
revolt: env!("CARGO_PKG_VERSION").to_string(),
features: RevoltFeatures {
captcha: CaptchaFeature {
enabled: *USE_HCAPTCHA,
+1 -1
View File
@@ -1,5 +1,5 @@
use revolt_quark::models::User;
use revolt_quark::r#impl::generic::users::user_settings::UserSettingsImpl;
use revolt_quark::r#impl::UserSettingsImpl;
use revolt_quark::{Db, EmptyResponse, Result};
use chrono::prelude::*;
+10 -6
View File
@@ -1,16 +1,20 @@
use revolt_quark::models::User;
use revolt_quark::{Database, Error, Result};
use revolt_quark::{Database, Error, Ref, Result};
use rocket::serde::json::Json;
use rocket::State;
/// # Send Friend Request / Accept Request
/// # Accept Friend Request
///
/// Send a friend request to another user or accept another user's friend request.
/// Accept another user's friend request.
#[openapi(tag = "Relationships")]
#[put("/<username>/friend")]
pub async fn req(db: &State<Database>, user: User, username: String) -> Result<Json<User>> {
let mut target = db.fetch_user_by_username(&username).await?;
#[put("/<target>/friend")]
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<User>> {
let mut target = if let Ok(user) = db.fetch_user_by_username(&target.id).await {
user
} else {
target.as_user(db).await?
};
if user.bot.is_some() || target.bot.is_some() {
return Err(Error::IsBot);
@@ -1,6 +1,5 @@
use crate::util::regex::RE_USERNAME;
use rauth::entities::Account;
use revolt_quark::{models::User, Database, Error, Result};
use revolt_quark::{models::User, rauth::models::Account, Database, Error, Result};
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -59,18 +59,6 @@ impl rocket_okapi::response::OpenApiResponderInner for CachedFile {
pub async fn req(target: String) -> CachedFile {
CachedFile((
ContentType::PNG,
match target.chars().last().unwrap() {
// 0123456789ABCDEFGHJKMNPQRSTVWXYZ
'0' | '1' | '2' | '3' | 'S' | 'Z' => {
include_bytes!(crate::asset!("user/2.png")).to_vec()
}
'4' | '5' | '6' | '7' | 'T' => include_bytes!(crate::asset!("user/3.png")).to_vec(),
'8' | '9' | 'A' | 'B' => include_bytes!(crate::asset!("user/4.png")).to_vec(),
'C' | 'D' | 'E' | 'F' | 'V' => include_bytes!(crate::asset!("user/5.png")).to_vec(),
'G' | 'H' | 'J' | 'K' | 'W' => include_bytes!(crate::asset!("user/6.png")).to_vec(),
'M' | 'N' | 'P' | 'Q' | 'X' => include_bytes!(crate::asset!("user/7.png")).to_vec(),
/*'0' | '1' | '2' | '3' | 'R' | 'Y'*/
_ => include_bytes!(crate::asset!("user/1.png")).to_vec(),
},
revolt_quark::util::pfp::avatar(target.chars().last().unwrap()),
))
}
+2
View File
@@ -13,6 +13,7 @@ mod find_mutual;
mod get_default_avatar;
mod open_dm;
mod remove_friend;
mod send_friend_request;
mod unblock_user;
pub fn routes() -> (Vec<Route>, OpenApi) {
@@ -33,5 +34,6 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
remove_friend::req,
block_user::req,
unblock_user::req,
send_friend_request::req,
]
}
@@ -0,0 +1,32 @@
use revolt_quark::models::User;
use revolt_quark::{Database, Error, Result};
use rocket::serde::json::Json;
use rocket::State;
use serde::{Deserialize, Serialize};
/// # User Lookup Information
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct DataSendFriendRequest {
username: String,
}
/// # Send Friend Request
///
/// Send a friend request to another user.
#[openapi(tag = "Relationships")]
#[post("/friend", data = "<data>")]
pub async fn req(
db: &State<Database>,
user: User,
data: Json<DataSendFriendRequest>,
) -> Result<Json<User>> {
let mut target = db.fetch_user_by_username(&data.username).await?;
if user.bot.is_some() || target.bot.is_some() {
return Err(Error::IsBot);
}
user.add_friend(db, &mut target).await?;
Ok(Json(target.with_auto_perspective(db, &user).await))
}
-2
View File
@@ -1,3 +1 @@
pub mod idempotency;
pub mod ratelimiter;
pub mod regex;
-1
View File
@@ -1 +0,0 @@
pub const VERSION: &str = "0.5.3-5-patch.2";
+26 -8
View File
@@ -8,8 +8,20 @@ edition = "2021"
[features]
mongo = [ "mongodb" ]
rocket_impl = [ "rocket", "rocket_empty" ]
test = [ "async-std", "mongo", "mongodb/async-std-runtime", "rocket_impl", "rauth" ]
rocket_impl = [
"rocket",
"rocket_empty",
"rocket_cors",
"lru",
"dashmap",
"rauth/database-mongodb",
"rauth/rocket_impl",
"rauth/okapi_impl"
]
test = [ "async-std", "mongo", "mongodb/async-std-runtime", "rocket_impl" ]
default = [ "test" ]
[dependencies]
@@ -26,8 +38,8 @@ bson = { version = "2.1.0", features = ["chrono-0_4"] }
# Spec Generation
schemars = "0.8.8"
okapi = { git = "https://github.com/insertish/okapi", rev = "dcf0df115596ee07a587a7a543cddf3d7944645b" }
rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "dcf0df115596ee07a587a7a543cddf3d7944645b" }
okapi = { git = "https://github.com/insertish/okapi", rev = "a1048d0c8cd771e424ec97d33d825c32e06aa120" }
rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "a1048d0c8cd771e424ec97d33d825c32e06aa120" }
# okapi = "0.7.0-rc.1"
# rocket_okapi = "0.8.0-rc.1"
@@ -58,15 +70,21 @@ reqwest = "0.11.10"
bitfield = "0.13.2"
lazy_static = "1.4.0"
lru = { version = "0.7.6", optional = true }
dashmap = { version = "5.2.0", optional = true }
# Web Push
base64 = "0.13.0"
web-push = "0.7.2"
# Implementations
rauth = { optional = true, git = "https://github.com/insertish/rauth", rev = "001a9698c56cea79e69e4ae71d7bc2cb48aec1a6" }
rocket = { optional = true, version = "=0.5.0-rc.1", default-features = false, features = ["json"] }
rocket_http = { optional = true, version = "=0.5.0-rc.1" }
rocket_empty = { optional = true, git = "https://github.com/insertish/rocket_empty", branch = "rc1" }
rocket_http = { optional = true, version = "0.5.0-rc.2" }
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false, features = ["json"] }
rocket_empty = { optional = true, git = "https://github.com/insertish/rocket_empty", branch = "master" }
rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" }
# rAuth
rauth = { git = "https://github.com/insertish/rauth", rev = "d68558d3211268068a38aa5012ac284e679609c3", features = [ "async-std-runtime" ] }
# Sentry
sentry = "0.25.0"
@@ -0,0 +1,5 @@
You requested to have your account deleted, if you did not perform this action please take measures to secure your account immediately.
Confirm here: {{url}}
Sent by Revolt.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+12 -2
View File
@@ -1,8 +1,7 @@
use std::env;
use std::ops::Deref;
use crate::r#impl::dummy::DummyDb;
use crate::r#impl::mongo::MongoDb;
use crate::r#impl::{DummyDb, MongoDb};
use crate::AbstractDatabase;
/// Database information to use to create a client
@@ -61,3 +60,14 @@ impl Deref for Database {
}
}
}
impl From<Database> for rauth::Database {
fn from(val: Database) -> Self {
match val {
Database::Dummy(_) => rauth::Database::default(),
Database::MongoDb(MongoDb(client)) => {
rauth::Database::MongoDb(rauth::database::MongoDb(client.database("revolt")))
}
}
}
}
@@ -2,6 +2,7 @@ use std::collections::HashSet;
use serde_json::json;
use ulid::Ulid;
use validator::Validate;
use crate::{
events::client::EventV1,
@@ -17,7 +18,7 @@ use crate::{
january::{Embed, Text},
push::PushNotification,
},
Database, Result,
Database, Error, Result,
};
impl Message {
@@ -232,6 +233,9 @@ impl From<SystemMessage> for String {
impl SendableEmbed {
pub async fn into_embed(self, db: &Database, message_id: String) -> Result<Embed> {
self.validate()
.map_err(|error| Error::FailedValidation { error })?;
let media = if let Some(id) = self.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &message_id)
+28 -72
View File
@@ -8,9 +8,6 @@ use crate::{perms, Database, Error, Result};
use futures::try_join;
use impl_ops::impl_op_ex_commutative;
use okapi::openapi3::{SecurityScheme, SecuritySchemeData};
use rocket_okapi::gen::OpenApiGenerator;
use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use std::ops;
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
@@ -155,8 +152,36 @@ impl User {
/// Update a user's username
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
// Trim surrounding spaces
let username = username.trim().to_string();
// Make sure username is still at least 3 characters
if username.len() < 2 {
return Err(Error::InvalidUsername);
}
// Copy the username for validation
let username_lowercase = username.to_lowercase();
// Ensure the username itself isn't blocked
const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt"];
for username in BLOCKED_USERNAMES {
if username_lowercase == *username {
return Err(Error::InvalidUsername);
}
}
// Ensure none of the following substrings show up in the username
const BLOCKED_SUBSTRINGS: &[&str] = &["@", "#", ":", "```", "\n"];
for substr in BLOCKED_SUBSTRINGS {
if username_lowercase.contains(substr) {
return Err(Error::InvalidUsername);
}
}
// Make sure the username isn't taken
if db.is_username_taken(&username).await? {
return Err(Error::UsernameTaken);
}
@@ -359,72 +384,3 @@ impl User {
}
}
}
use rauth::entities::Session;
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
#[rocket::async_trait]
impl<'r> FromRequest<'r> for User {
type Error = rauth::util::Error;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let user: &Option<User> = request
.local_cache_async(async {
let db = request
.rocket()
.state::<Database>()
.expect("Database state not reachable!");
let header_bot_token = request
.headers()
.get("x-bot-token")
.next()
.map(|x| x.to_string());
if let Some(bot_token) = header_bot_token {
if let Ok(user) = User::from_token(db, &bot_token, UserHint::Bot).await {
return Some(user);
}
} else if let Outcome::Success(session) = request.guard::<Session>().await {
// This uses a guard so can't really easily be refactored into from_token at this stage.
if let Ok(user) = db.fetch_user(&session.user_id).await {
return Some(user);
}
}
None
})
.await;
if let Some(user) = user {
Outcome::Success(user.clone())
} else {
Outcome::Failure((Status::Unauthorized, rauth::util::Error::InvalidSession))
}
}
}
impl<'r> OpenApiFromRequest<'r> for User {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> rocket_okapi::Result<RequestHeaderInput> {
let mut requirements = schemars::Map::new();
requirements.insert("Api Key".to_owned(), vec![]);
Ok(RequestHeaderInput::Security(
"Api Key".to_owned(),
SecurityScheme {
data: SecuritySchemeData::ApiKey {
name: "x-session-token".to_owned(),
location: "header".to_owned(),
},
description: Some("Session Token".to_owned()),
extensions: schemars::Map::new(),
},
requirements,
))
}
}
+10 -3
View File
@@ -1,3 +1,10 @@
pub mod dummy;
pub mod generic;
pub mod mongo;
mod dummy;
mod generic;
mod mongo;
#[cfg(feature = "rocket_impl")]
mod rocket;
pub use self::generic::users::user_settings::UserSettingsImpl;
pub use dummy::DummyDb;
pub use mongo::MongoDb;
@@ -2,7 +2,6 @@ use crate::r#impl::mongo::MongoDb;
use super::scripts::LATEST_REVISION;
use log::info;
use mongodb::bson::doc;
use mongodb::options::CreateCollectionOptions;
@@ -2,7 +2,6 @@ use std::time::Duration;
use bson::Bson;
use futures::StreamExt;
use log::info;
use mongodb::{
bson::{doc, from_bson, from_document, to_document, Document},
options::FindOptions,
@@ -17,7 +16,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 15;
pub const LATEST_REVISION: i32 = 16;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -603,6 +602,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.unwrap();
}
if revision <= 15 {
info!("Running migration [revision 15 / 04-06-2022]: Migrate rAuth to latest version.");
let db = rauth::Database::MongoDb(rauth::database::MongoDb(db.db()));
db.run_migration(rauth::Migration::M2022_06_03EnsureUpToSpec)
.await
.unwrap();
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations.
+73
View File
@@ -0,0 +1,73 @@
use okapi::openapi3::{SecurityScheme, SecuritySchemeData};
use rauth::models::Session;
use rocket_okapi::gen::OpenApiGenerator;
use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
use crate::models::user::UserHint;
use crate::models::User;
use crate::Database;
#[rocket::async_trait]
impl<'r> FromRequest<'r> for User {
type Error = rauth::Error;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let user: &Option<User> = request
.local_cache_async(async {
let db = request.rocket().state::<Database>().expect("`Database`");
let header_bot_token = request
.headers()
.get("x-bot-token")
.next()
.map(|x| x.to_string());
if let Some(bot_token) = header_bot_token {
if let Ok(user) = User::from_token(db, &bot_token, UserHint::Bot).await {
return Some(user);
}
} else if let Outcome::Success(session) = request.guard::<Session>().await {
// This uses a guard so can't really easily be refactored into from_token at this stage.
if let Ok(user) = db.fetch_user(&session.user_id).await {
return Some(user);
}
}
None
})
.await;
if let Some(user) = user {
Outcome::Success(user.clone())
} else {
Outcome::Failure((Status::Unauthorized, rauth::Error::InvalidSession))
}
}
}
impl<'r> OpenApiFromRequest<'r> for User {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> rocket_okapi::Result<RequestHeaderInput> {
let mut requirements = schemars::Map::new();
requirements.insert("Session Token".to_owned(), vec![]);
Ok(RequestHeaderInput::Security(
"Session Token".to_owned(),
SecurityScheme {
data: SecuritySchemeData::ApiKey {
name: "x-session-token".to_owned(),
location: "header".to_owned(),
},
description: Some("Used to authenticate as a user.".to_owned()),
extensions: schemars::Map::new(),
},
requirements,
))
}
}
+19 -33
View File
@@ -15,9 +15,10 @@ extern crate lazy_static;
#[macro_use]
extern crate bitfield;
#[macro_use]
pub extern crate bson;
extern crate bson;
pub use iso8601_timestamp::Timestamp;
pub use rauth;
pub use redis_kiss;
pub mod events;
@@ -26,11 +27,14 @@ pub mod models;
pub mod presence;
pub mod tasks;
pub mod types;
pub mod util;
#[cfg(feature = "rocket_impl")]
pub mod web;
mod database;
mod permissions;
mod traits;
mod util;
pub use database::*;
pub use traits::*;
@@ -38,38 +42,20 @@ pub use traits::*;
pub use permissions::defn::*;
pub use permissions::{get_relationship, perms};
pub use util::r#ref::Ref;
pub use util::result::{Error, Result};
pub use util::variables;
pub use util::{
r#ref::Ref,
result::{Error, Result},
variables,
};
#[cfg(feature = "rocket_impl")]
pub use rocket_empty::EmptyResponse;
pub use web::{Db, EmptyResponse};
#[cfg(feature = "rocket_impl")]
use rocket::State;
#[cfg(feature = "rocket_impl")]
pub type Db = State<Database>;
/// Configure logging and common Rust variables
pub fn setup_logging() -> sentry::ClientInitGuard {
dotenv::dotenv().ok();
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
if std::env::var("ROCKET_ADDRESS").is_err() {
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
}
pretty_env_logger::init();
sentry::init((
"https://62fd0e02c5354905b4e286757f4beb16@sentry.insert.moe/4",
sentry::ClientOptions {
release: sentry::release_name!(),
..Default::default()
},
))
/// Resolve asset
macro_rules! asset {
($path:literal) => {
concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $path)
};
}
pub(crate) use asset;
@@ -2,7 +2,6 @@
use crate::{models::channel::PartialChannel, Database};
use deadqueue::limited::Queue;
use log::info;
use mongodb::bson::doc;
use std::{collections::HashMap, time::Duration};
+1 -1
View File
@@ -18,7 +18,7 @@ pub async fn start_workers(db: Database) {
task::spawn(ack::worker(db.clone()));
task::spawn(last_message_id::worker(db.clone()));
task::spawn(process_embeds::worker(db.clone()));
task::spawn(web_push::worker(db.clone()));
task::spawn(web_push::worker(db.clone().into()));
}
}
-1
View File
@@ -6,7 +6,6 @@ use crate::{
};
use deadqueue::limited::Queue;
use log::error;
/// Task information
#[derive(Debug)]
+5 -27
View File
@@ -1,9 +1,8 @@
use crate::bson::doc;
use crate::util::variables::delta::VAPID_PRIVATE_KEY;
use crate::{bson::doc, r#impl::mongo::MongoDb, Database};
use deadqueue::limited::Queue;
use futures::StreamExt;
use rauth::entities::Session;
use rauth::Database;
use web_push::{
ContentEncoding, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, WebPushClient,
WebPushMessageBuilder,
@@ -43,28 +42,11 @@ pub async fn worker(db: Database) {
let key = base64::decode_config(VAPID_PRIVATE_KEY.clone(), base64::URL_SAFE)
.expect("valid `VAPID_PRIVATE_KEY`");
if let Database::MongoDb(MongoDb(db)) = db {
loop {
let task = Q.pop().await;
// ! FIXME: this is hard-coded until rauth is merged into quark
if let Ok(mut cursor) = db
.database("revolt")
.collection::<Session>("sessions")
.find(
doc! {
"user_id": {
"$in": task.recipients
},
"subscription": {
"$exists": true
}
},
None,
)
.await
{
while let Some(Ok(session)) = cursor.next().await {
if let Ok(sessions) = db.find_sessions_with_subscription(&task.recipients).await {
for session in sessions {
if let Some(sub) = session.subscription {
let subscription = SubscriptionInfo {
endpoint: sub.endpoint,
@@ -97,10 +79,7 @@ pub async fn worker(db: Database) {
)
}
Err(err) => {
error!(
"Hit error sending Web Push! {:?}",
err
)
error!("Hit error sending Web Push! {:?}", err)
}
},
Err(err) => {
@@ -131,5 +110,4 @@ pub async fn worker(db: Database) {
}
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
/// Configure logging and common Rust variables
pub fn setup_logging(release: &'static str) -> sentry::ClientInitGuard {
dotenv::dotenv().ok();
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
if std::env::var("ROCKET_ADDRESS").is_err() {
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
}
pretty_env_logger::init();
info!("Starting {release}");
sentry::init((
"https://62fd0e02c5354905b4e286757f4beb16@sentry.insert.moe/4",
sentry::ClientOptions {
release: Some(release.into()),
..Default::default()
},
))
}
#[macro_export]
macro_rules! configure {
() => {
let _sentry = revolt_quark::util::log::setup_logging(concat!(
env!("CARGO_PKG_NAME"),
"@",
env!("CARGO_PKG_VERSION")
));
};
}
+3
View File
@@ -1,4 +1,7 @@
pub mod log;
pub mod manipulation;
pub mod pfp;
pub mod rauth;
pub mod r#ref;
pub mod result;
pub mod value;
+13
View File
@@ -0,0 +1,13 @@
pub fn avatar(v: char) -> Vec<u8> {
match v {
// 0123456789ABCDEFGHJKMNPQRSTVWXYZ
'0' | '1' | '2' | '3' | 'S' | 'Z' => include_bytes!(crate::asset!("user/2.png")).to_vec(),
'4' | '5' | '6' | '7' | 'T' => include_bytes!(crate::asset!("user/3.png")).to_vec(),
'8' | '9' | 'A' | 'B' => include_bytes!(crate::asset!("user/4.png")).to_vec(),
'C' | 'D' | 'E' | 'F' | 'V' => include_bytes!(crate::asset!("user/5.png")).to_vec(),
'G' | 'H' | 'J' | 'K' | 'W' => include_bytes!(crate::asset!("user/6.png")).to_vec(),
'M' | 'N' | 'P' | 'Q' | 'X' => include_bytes!(crate::asset!("user/7.png")).to_vec(),
/*'0' | '1' | '2' | '3' | 'R' | 'Y'*/
_ => include_bytes!(crate::asset!("user/1.png")).to_vec(),
}
}
+63
View File
@@ -0,0 +1,63 @@
use super::variables::delta::{
APP_URL, HCAPTCHA_KEY, INVITE_ONLY, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_USERNAME,
USE_EMAIL, USE_HCAPTCHA,
};
use crate::rauth::config::{
Captcha, Config, EmailVerificationConfig, SMTPSettings, Template, Templates,
};
pub fn config() -> Config {
let mut config = Config {
email_verification: if *USE_EMAIL {
EmailVerificationConfig::Enabled {
smtp: SMTPSettings {
from: (*SMTP_FROM).to_string(),
host: (*SMTP_HOST).to_string(),
username: (*SMTP_USERNAME).to_string(),
password: (*SMTP_PASSWORD).to_string(),
reply_to: Some("support@revolt.chat".into()),
port: None,
use_tls: None,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!(crate::asset!("templates/verify.txt")).into(),
url: format!("{}/login/verify/", *APP_URL),
html: None,
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!(crate::asset!("templates/reset.txt")).into(),
url: format!("{}/login/reset/", *APP_URL),
html: None,
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!(crate::asset!("templates/deletion.txt")).into(),
url: format!("{}/delete/", *APP_URL),
html: None,
},
welcome: None,
},
}
} else {
EmailVerificationConfig::Disabled
},
..Default::default()
};
if *INVITE_ONLY {
config.invite_only = true;
}
if *USE_HCAPTCHA {
config.captcha = Captcha::HCaptcha {
secret: HCAPTCHA_KEY.clone(),
};
}
config
}
+2
View File
@@ -24,6 +24,7 @@ pub enum Error {
// ? User related errors.
UsernameTaken,
InvalidUsername,
UnknownUser,
AlreadyFriends,
AlreadySentRequest,
@@ -128,6 +129,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::AlreadyOnboarded => Status::Forbidden,
Error::UnknownUser => Status::NotFound,
Error::InvalidUsername => Status::BadRequest,
Error::UsernameTaken => Status::Conflict,
Error::AlreadyFriends => Status::Conflict,
Error::AlreadySentRequest => Status::Conflict,
-3
View File
@@ -1,8 +1,5 @@
use std::env;
#[cfg(debug_assertions)]
use log::warn;
lazy_static! {
// Application Settings
pub static ref PUBLIC_URL: String =
+19
View File
@@ -0,0 +1,19 @@
use std::str::FromStr;
pub use rocket_cors::catch_all_options_routes;
use rocket_cors::{AllowedOrigins, Cors};
pub fn new() -> Cors {
rocket_cors::CorsOptions {
allowed_origins: AllowedOrigins::All,
allowed_methods: [
"Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch",
]
.iter()
.map(|s| FromStr::from_str(s).unwrap())
.collect(),
..Default::default()
}
.to_cors()
.expect("Failed to create CORS.")
}
@@ -1,6 +1,6 @@
use crate::{Error, Result};
use async_std::sync::Mutex;
use mongodb::bson::doc;
use revolt_quark::{Error, Result};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use rocket_okapi::gen::OpenApiGenerator;
+10
View File
@@ -0,0 +1,10 @@
use crate::Database;
use rocket::State;
pub mod cors;
pub mod idempotency;
pub mod ratelimiter;
pub mod swagger;
pub use rocket_empty::EmptyResponse;
pub type Db = State<Database>;
@@ -8,6 +8,7 @@ use std::hash::Hasher;
use std::ops::Add;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::rauth::models::Session;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::uri::Origin;
use rocket::http::{Method, Status};
@@ -22,8 +23,6 @@ use serde::Serialize;
use dashmap::DashMap;
use log::info;
/// Ratelimit Bucket
#[derive(Clone, Copy)]
struct Entry {
@@ -207,10 +206,9 @@ impl<'r> FromRequest<'r> for Ratelimiter {
let ratelimiter = request
.local_cache_async(async {
use rocket::outcome::Outcome;
let identifier = if let Outcome::Success(session) =
request.guard::<rauth::entities::Session>().await
let identifier = if let Outcome::Success(session) = request.guard::<Session>().await
{
session.id.expect("`id` on User")
session.id
} else {
to_real_ip(request)
};
@@ -304,11 +302,11 @@ impl<'r> FromRequest<'r> for RatelimitInformation {
}
}
#[get("/ratelimit")]
#[rocket::get("/ratelimit")]
fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
Json(info)
}
pub fn routes() -> Vec<rocket::Route> {
routes![ratelimit_info]
rocket::routes![ratelimit_info]
}
+9
View File
@@ -0,0 +1,9 @@
use rocket::Route;
pub fn routes() -> Vec<Route> {
rocket_okapi::swagger_ui::make_swagger_ui(&rocket_okapi::swagger_ui::SwaggerUIConfig {
url: "../openapi.json".to_owned(),
..Default::default()
})
.into()
}
+18
View File
@@ -0,0 +1,18 @@
version: "3.3"
services:
redis:
image: eqalpha/keydb
ports:
- "6379:6379"
database:
image: mongo
ports:
- "27017:27017"
volumes:
- ./.data/db:/data/db
mongo-express:
image: mongo-express
ports:
- "8081:8081"
environment:
- ME_CONFIG_MONGODB_SERVER=database