Compare commits

...
31 Commits
Author SHA1 Message Date
Martin Löffler 73443ce883 revert: bump rauth for acc. verification fix
the "fix" caused emails to not send
2022-06-23 01:30:54 +02:00
Paul Makles 47297e243f fix: enforce min. password length
fixes #188
2022-06-21 11:06:02 +01:00
Paul Makles 4188b6d2f2 fix: ignore anchor links when parsing messages
fixes #183
2022-06-21 10:38:59 +01:00
Paul Makles 165380c7b4 chore: bump rauth for acc. verification fix 2022-06-21 10:28:14 +01:00
Paul Makles 49ca24ac9f chore: further restrict usernames
closes #186
2022-06-21 10:27:58 +01:00
Paul Makles 35f956ce7d fix: add separate bucket for default_avatar
closes #166
2022-06-20 11:13:15 +01:00
Paul Makles 4baab5d5d5 feat(messaging): cap total text content to 2k characters 2022-06-20 11:09:14 +01:00
Paul Makles 7fc4fb2df7 fix: rewrite attachment deletion logic 2022-06-20 10:49:09 +01:00
Paul Makles ef757aa2fb chore: bump quark 2022-06-20 10:27:26 +01:00
Paul Makles 0585dd0c20 fix: consistent username validation across routes
fixes #187
2022-06-20 10:27:22 +01:00
Paul Makles f96541efab fix: add additional validation on legacy nonce value 2022-06-14 17:35:44 +01:00
Paul Makles c6414338b6 fix: marking server as read would not mark it as read
fixes #169

Porting code forwards from an older revision of the codebase; https://github.com/revoltchat/backend/blob/0.5.3-alpha.10/src/database/entities/server.rs
2022-06-14 17:32:43 +01:00
Paul Makles 4c4eb60cdb fix: don't allow members to be added more than once
fixes #182
2022-06-14 17:27:15 +01:00
Paul Makles 11d89b3bf0 feat: enable 2FA login 2022-06-12 18:50:30 +01:00
Paul Makles 64bb171cc8 fix: remove test flag from rauth 2022-06-12 18:03:49 +01:00
Paul Makles 6de5ad15c5 chore: bump rauth 2022-06-12 17:48:12 +01:00
Paul Makles b5ab16d66f fix: bump quark to fix is_disabled check 2022-06-12 17:07:22 +01:00
Paul Makles b9aad6d38c chore: bump rauth 2022-06-11 20:49:19 +01:00
Paul Makles ed5c8159e9 chore: bump rauth 2022-06-10 17:47:13 +01:00
Paul Makles 2e6e1fdd44 chore: bump quark; changes deletion period 2022-06-10 17:22:34 +01:00
Paul Makles cb2e6fb2f7 feat: separate friend request route 2022-06-09 17:02:01 +01:00
Paul Makles d069acd5a5 chore: expose correct version in api spec 2022-06-09 16:05:04 +01:00
Paul Makles f8b5c25c17 chore: bump rauth; adds account deletion
closes #159
2022-06-09 16:00:59 +01:00
Paul Makles 80af18761f feat: add further restrictions on usernames 2022-06-09 14:06:13 +01:00
Paul Makles 98e10c3478 fix: bump rauth with assertion fix 2022-06-08 14:28:58 +01:00
Paul Makles 2051c8ce45 chore(refactor): validate further up the code 2022-06-08 14:14:16 +01:00
Paul Makles 0abc12e5fa chore(refactor): process attachments last 2022-06-08 14:12:11 +01:00
Paul Makles aaceb5bdee fix: sub-validate message structs 2022-06-08 14:11:24 +01:00
Paul Makles 3de7ee4c7b fix: cors needs to be attached 2022-06-08 14:08:33 +01:00
Paul Makles d660127c14 chore: refactor generic web server code into quark 2022-06-08 14:08:33 +01:00
Paul Makles 7390b3c087 feat: update to rAuth v1 2022-06-08 14:08:33 +01:00
69 changed files with 1067 additions and 1229 deletions
+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 = "c2acaf6b31213e3969f8b48f9deed3daa125d394" }
# 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;
@@ -36,6 +36,8 @@ pub async fn req(
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
Message::validate_sum(&edit.content, &edit.embeds)?;
let mut message = msg.as_message(db).await?;
if message.channel != target {
return Err(Error::NotFound);
@@ -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,13 +16,12 @@ 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
///
/// **This is deprecated and replaced by `Idempotency-Key`!**
#[validate(length(min = 1, max = 64))]
nonce: Option<String>,
/// Message content to send
@@ -32,6 +33,8 @@ pub struct DataMessageSend {
/// Messages to reply to
replies: Option<Vec<Reply>>,
/// Embeds to include in message
///
/// Text embed content contributes to the content length cap
#[validate(length(min = 1, max = 10))]
embeds: Option<Vec<SendableEmbed>>,
/// Masquerade to apply to this message
@@ -60,6 +63,8 @@ pub async fn message_send(
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
Message::validate_sum(&data.content, &data.embeds)?;
idempotency.consume_nonce(data.nonce).await?;
let channel = target.as_channel(db).await?;
@@ -134,7 +139,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 +177,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(
+3 -7
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;
@@ -33,13 +32,10 @@ pub async fn req(
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if db.is_username_taken(&data.username).await? {
return Err(Error::UsernameTaken);
}
let username = User::validate_username(db, data.username).await?;
let user = User {
id: session.user_id,
username: data.username,
username,
..Default::default()
};
+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 -1
View File
@@ -6,4 +6,4 @@ use regex::Regex;
/// Block zero width space
/// Block lookalike characters
pub static RE_USERNAME: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[^\u200BА-Яа-яΑ-Ωα-ω]+$").unwrap());
Lazy::new(|| Regex::new(r"^[^\u200BА-Яа-яΑ-Ωα-ω@#:\n\r\[\]]+$").unwrap());
-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 = "c2acaf6b31213e3969f8b48f9deed3daa125d394", 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.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

+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")))
}
}
}
}
@@ -39,4 +39,9 @@ impl AbstractAttachment for DummyDb {
info!("Marked {id} as deleted");
Ok(())
}
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
info!("Marked {ids:?} as deleted");
Ok(())
}
}
@@ -242,7 +242,12 @@ impl Channel {
/// Add user to a group
pub async fn add_user_to_group(&mut self, db: &Database, user: &str, by: &str) -> Result<()> {
if let Channel::Group { recipients, .. } = self {
recipients.push(user.to_string());
let user = user.to_string();
if recipients.contains(&user) {
return Err(Error::AlreadyInGroup);
}
recipients.push(user);
}
match &self {
@@ -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 {
@@ -134,7 +135,17 @@ impl Message {
/// Delete a message
pub async fn delete(self, db: &Database) -> Result<()> {
let file_ids: Vec<String> = self
.attachments
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
.unwrap_or_default();
if !file_ids.is_empty() {
db.mark_attachments_as_deleted(&file_ids).await?;
}
db.delete_message(&self.id).await?;
EventV1::MessageDelete {
id: self.id,
channel: self.channel.clone(),
@@ -155,6 +166,31 @@ impl Message {
.await;
Ok(())
}
/// Validate the sum of content of a message is under threshold
pub fn validate_sum(
content: &Option<String>,
embeds: &Option<Vec<SendableEmbed>>,
) -> Result<()> {
let mut running_total = 0;
if let Some(content) = content {
running_total += content.len();
}
if let Some(embeds) = embeds {
for embed in embeds {
if let Some(desc) = &embed.description {
running_total += desc.len();
}
}
}
if running_total <= 2000 {
Ok(())
} else {
Err(Error::PayloadTooLarge)
}
}
}
pub trait IntoUsers {
@@ -232,6 +268,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)
+36 -75
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 });
@@ -153,18 +150,51 @@ impl User {
Ok(db.fetch_server_count(&self.id).await? <= 100)
}
/// Update a user's username
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
/// Sanitise and validate a username can be used
pub async fn validate_username(db: &Database, username: String) -> Result<String> {
// 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] = &["```"];
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);
}
Ok(username)
}
/// Update a user's username
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
self.update(
db,
PartialUser {
username: Some(username),
username: Some(User::validate_username(db, username).await?),
..Default::default()
},
vec![],
@@ -359,72 +389,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.
@@ -122,4 +122,27 @@ impl AbstractAttachment for MongoDb {
with: "attachment",
})
}
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.update_many(
doc! {
"_id": {
"$in": ids
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update",
with: "attachments",
})
}
}
@@ -37,30 +37,46 @@ impl AbstractChannelUnread for MongoDb {
}
async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
self.col::<Document>(COL)
.update_one(
.delete_many(
doc! {
"_id.channel": {
"$in": channels
},
"_id.user": user,
"_id.user": user
},
doc! {
"$unset": {
"mentions": 1_i32
},
"$set": {
"last_id": Ulid::new().to_string()
}
},
UpdateOptions::builder().upsert(true).build(),
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update",
with: "channel_unread",
operation: "delete_many",
with: "channel_unreads",
})?;
self.col::<Document>(COL)
.insert_many(
channels
.iter()
.map(|channel| {
doc! {
"_id": {
"channel": channel,
"user": user
},
"last_id": &current_time
}
})
.collect::<Vec<Document>>(),
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "channel_unreads",
})
.map(|_| ())
}
async fn add_mention_to_unread<'a>(
@@ -14,7 +14,7 @@ impl MongoDb {
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
let mut for_attachments = projection.clone();
for_attachments.insert(
"attachment",
"attachments",
doc! {
"$exists": 1_i32
},
@@ -126,10 +126,7 @@ impl AbstractMessage for MongoDb {
}
async fn delete_message(&self, id: &str) -> Result<()> {
self.delete_bulk_messages(doc! {
"_id": id
})
.await
self.delete_one_by_id(COL, id).await.map(|_| ())
}
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()> {
+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)]
+52 -74
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,90 +42,69 @@ 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;
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
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,
keys: SubscriptionKeys {
auth: sub.auth,
p256dh: sub.p256dh,
},
"subscription": {
"$exists": true
}
},
None,
)
.await
{
while let Some(Ok(session)) = cursor.next().await {
if let Some(sub) = session.subscription {
let subscription = SubscriptionInfo {
endpoint: sub.endpoint,
keys: SubscriptionKeys {
auth: sub.auth,
p256dh: sub.p256dh,
},
};
};
match WebPushMessageBuilder::new(&subscription) {
Ok(mut builder) => {
match VapidSignatureBuilder::from_pem(
std::io::Cursor::new(&key),
&subscription,
) {
Ok(sig_builder) => match sig_builder.build() {
Ok(signature) => {
builder.set_vapid_signature(signature);
builder.set_payload(
ContentEncoding::AesGcm,
task.payload.as_bytes(),
);
match WebPushMessageBuilder::new(&subscription) {
Ok(mut builder) => {
match VapidSignatureBuilder::from_pem(
std::io::Cursor::new(&key),
&subscription,
) {
Ok(sig_builder) => match sig_builder.build() {
Ok(signature) => {
builder.set_vapid_signature(signature);
builder.set_payload(
ContentEncoding::AesGcm,
task.payload.as_bytes(),
);
match builder.build() {
Ok(msg) => match client.send(msg).await {
Ok(_) => {
info!(
"Sent Web Push notification to {:?}.",
session.id
)
}
Err(err) => {
error!(
"Hit error sending Web Push! {:?}",
err
)
}
},
Err(err) => {
error!(
"Failed to build message for {}! {:?}",
session.user_id, err
match builder.build() {
Ok(msg) => match client.send(msg).await {
Ok(_) => {
info!(
"Sent Web Push notification to {:?}.",
session.id
)
}
Err(err) => {
error!("Hit error sending Web Push! {:?}", err)
}
},
Err(err) => {
error!(
"Failed to build message for {}! {:?}",
session.user_id, err
)
}
}
Err(err) => error!(
"Failed to build signature for {}! {:?}",
session.user_id, err
),
},
}
Err(err) => error!(
"Failed to create signature builder for {}! {:?}",
"Failed to build signature for {}! {:?}",
session.user_id, err
),
}
},
Err(err) => error!(
"Failed to create signature builder for {}! {:?}",
session.user_id, err
),
}
Err(err) => error!(
"Invalid subscription information for {}! {:?}",
session.user_id, err
),
}
Err(err) => error!(
"Invalid subscription information for {}! {:?}",
session.user_id, err
),
}
}
}
@@ -13,4 +13,5 @@ pub trait AbstractAttachment: Sync + Send {
async fn insert_attachment(&self, attachment: &File) -> Result<()>;
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()>;
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()>;
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()>;
}
+12 -2
View File
@@ -202,12 +202,22 @@ impl Embed {
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
let links: HashSet<String> = finder
// Process all links, stripping anchors and
// only taking up to `max_embeds` of links.
let links: Vec<String> = finder
.links(&content)
.map(|x| {
x.as_str()
.chars()
.take_while(|&ch| ch != '#')
.collect::<String>()
})
.collect::<HashSet<String>>()
.into_iter()
.take(max_embeds)
.map(|x| x.as_str().to_string())
.collect();
// If no links, fail out.
if links.is_empty() {
return Err(Error::LabelMe);
}
+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
}
+4
View File
@@ -24,6 +24,7 @@ pub enum Error {
// ? User related errors.
UsernameTaken,
InvalidUsername,
UnknownUser,
AlreadyFriends,
AlreadySentRequest,
@@ -40,6 +41,7 @@ pub enum Error {
TooManyAttachments,
TooManyReplies,
EmptyMessage,
PayloadTooLarge,
CannotRemoveYourself,
GroupTooLarge {
max: usize,
@@ -128,6 +130,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,
@@ -143,6 +146,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::TooManyAttachments => Status::BadRequest,
Error::TooManyReplies => Status::BadRequest,
Error::EmptyMessage => Status::UnprocessableEntity,
Error::PayloadTooLarge => Status::UnprocessableEntity,
Error::CannotRemoveYourself => Status::BadRequest,
Error::GroupTooLarge { .. } => Status::Forbidden,
Error::AlreadyInGroup => 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 {
@@ -101,7 +100,13 @@ fn resolve_bucket<'r>(request: &'r rocket::Request<'_>) -> (&'r str, Option<&'r
if let Some(segment) = request.routed_segment(0) {
let resource = request.routed_segment(1);
match (segment, resource) {
("users", _) => ("users", None),
("users", _) => {
if let Some("default_avatar") = request.routed_segment(2) {
return ("default_avatar", None);
}
("users", None)
}
("bots", _) => ("bots", None),
("channels", Some(id)) => {
if request.method() == Method::Post {
@@ -138,6 +143,7 @@ fn resolve_bucket_limit(bucket: &str) -> u8 {
"servers" => 5,
"auth" => 15,
"auth_delete" => 255,
"default_avatar" => 255,
"swagger" => 100,
_ => 20,
}
@@ -207,10 +213,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 +309,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()
}
+73
View File
@@ -0,0 +1,73 @@
version: "3.3"
services:
# Redis
redis:
image: eqalpha/keydb
ports:
- "6379:6379"
# MongoDB
database:
image: mongo
ports:
- "27017:27017"
volumes:
- ./.data/db:/data/db
# MinIO
minio:
image: minio/minio
command: server /data
env_file: .env
volumes:
- ./.data/minio:/data
ports:
- "10000:9000"
restart: always
# Mongo Express
mongo-express:
image: mongo-express
ports:
- "8081:8081"
environment:
- ME_CONFIG_MONGODB_SERVER=database
depends_on:
- database
# Create buckets for minio.
createbuckets:
image: minio/mc
depends_on:
- minio
env_file: .env
entrypoint: >
/bin/sh -c "
while ! curl -s --output /dev/null --connect-timeout 1 http://minio:9000; do echo 'Waiting minio...' && sleep 0.1; done;
/usr/bin/mc alias set minio http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD;
/usr/bin/mc mb minio/attachments;
/usr/bin/mc mb minio/avatars;
/usr/bin/mc mb minio/backgrounds;
/usr/bin/mc mb minio/icons;
/usr/bin/mc mb minio/banners;
exit 0;
"
# File server (autumn)
autumn:
image: ghcr.io/revoltchat/autumn:1.1.4
env_file: .env
depends_on:
- database
- createbuckets
environment:
- AUTUMN_MONGO_URI=mongodb://database
ports:
- "3000:3000"
restart: always
# Metadata and image proxy (january)
january:
image: ghcr.io/revoltchat/january:0.3.4
ports:
- "7000:7000"
restart: always