Merge branch 'master' into msgpack_fix

This commit is contained in:
Paul Makles
2021-09-15 21:12:27 +01:00
committed by GitHub
39 changed files with 1103 additions and 896 deletions

View File

@@ -17,6 +17,8 @@ on:
pull_request: pull_request:
branches: branches:
- "master" - "master"
paths:
- "Dockerfile"
workflow_dispatch: workflow_dispatch:
jobs: jobs:

33
.github/workflows/rust.yaml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Rust build and test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
check:
name: Rust project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install latest nightly
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
components: rustfmt, clippy
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test

1021
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ edition = "2018"
[dependencies] [dependencies]
# Utility # Utility
url = "2.2.2"
log = "0.4.11" log = "0.4.11"
dotenv = "0.15.0" dotenv = "0.15.0"
linkify = "0.6.0" linkify = "0.6.0"
@@ -18,7 +19,6 @@ once_cell = "1.4.1"
env_logger = "0.7.1" env_logger = "0.7.1"
lazy_static = "1.4.0" lazy_static = "1.4.0"
ctrlc = { version = "3.0", features = ["termination"] } ctrlc = { version = "3.0", features = ["termination"] }
url = "2.2.2"
# Lang. Utilities # Lang. Utilities
regex = "1" regex = "1"
@@ -48,7 +48,7 @@ async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"]
web-push = "0.7.2" web-push = "0.7.2"
many-to-many = "0.1.2" many-to-many = "0.1.2"
lettre = "0.10.0-alpha.4" lettre = "0.10.0-alpha.4"
rauth = { git = "https://github.com/insertish/rauth", rev = "19bef300ecc269c028e3d746f0a2f09842d3b37b" } rauth = { git = "https://github.com/insertish/rauth", rev = "ad2f5ad1657f4b20c8e483cc1d6c513ff9b22ae1" }
hive_pubsub = { git = "https://gitlab.insrt.uk/insert/hive", rev = "a89826df2b30166220e68a6ed01a58b751456604", features = ["mongo"] } hive_pubsub = { git = "https://gitlab.insrt.uk/insert/hive", rev = "a89826df2b30166220e68a6ed01a58b751456604", features = ["mongo"] }
# web # web

View File

@@ -0,0 +1,5 @@
You requested a password reset, if you did not perform this action you can safely ignore this email.
Reset your password here: {{url}}
Sent by Revolt.

View File

@@ -0,0 +1,6 @@
You're almost there!
If you did not perform this action you can safely ignore this email.
Please verify your account here: {{url}}
Sent by Revolt.

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
export version=0.5.2-alpha.2 export version=0.5.3-alpha.1
echo "pub const VERSION: &str = \"${version}\";" > src/version.rs echo "pub const VERSION: &str = \"${version}\";" > src/version.rs

View File

@@ -13,14 +13,6 @@ use mongodb::{
use rocket::serde::json::Value; use rocket::serde::json::Value;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LastMessage {
#[serde(rename = "_id")]
id: String,
author: String,
short: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "channel_type")] #[serde(tag = "channel_type")]
pub enum Channel { pub enum Channel {
@@ -36,7 +28,7 @@ pub enum Channel {
active: bool, active: bool,
recipients: Vec<String>, recipients: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
last_message: Option<LastMessage>, last_message_id: Option<String>,
}, },
Group { Group {
#[serde(rename = "_id")] #[serde(rename = "_id")]
@@ -53,7 +45,7 @@ pub enum Channel {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>, icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
last_message: Option<LastMessage>, last_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
permissions: Option<i32>, permissions: Option<i32>,
@@ -75,7 +67,7 @@ pub enum Channel {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>, icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
last_message: Option<String>, last_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<i32>, default_permissions: Option<i32>,

View File

@@ -8,15 +8,13 @@ use crate::{
use futures::StreamExt; use futures::StreamExt;
use mongodb::options::UpdateOptions; use mongodb::options::UpdateOptions;
use mongodb::{ use mongodb::{
bson::{doc, to_bson, DateTime}, bson::{doc, to_bson, DateTime, Document},
options::FindOptions,
}; };
use rauth::entities::{Model, Session};
use rocket::serde::json::Value; use rocket::serde::json::Value;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ulid::Ulid; use ulid::Ulid;
use web_push::{ use web_push::{ContentEncoding, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder};
ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder,
};
use std::time::SystemTime; use std::time::SystemTime;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@@ -173,7 +171,6 @@ impl Message {
nonce: None, nonce: None,
channel, channel,
author, author,
content, content,
attachments: None, attachments: None,
edited: None, edited: None,
@@ -198,76 +195,19 @@ impl Message {
let ss = self.clone(); let ss = self.clone();
let c_clone = channel.clone(); let c_clone = channel.clone();
async_std::task::spawn(async move { async_std::task::spawn(async move {
let mut set = if let Content::Text(text) = &ss.content {
doc! {
"last_message": {
"_id": ss.id.clone(),
"author": ss.author.clone(),
"short": text.chars().take(128).collect::<String>()
}
}
} else {
doc! {}
};
// ! MARK AS ACTIVE let last_message_id = ss.id.clone();
// ! FIXME: temp code let mut set = doc! { "last_message_id": last_message_id };
let channels = get_collection("channels"); let channels = get_collection("channels");
match &c_clone { match &c_clone {
Channel::DirectMessage { id, .. } => { Channel::DirectMessage { id, .. } => {
// ! MARK AS ACTIVE
set.insert("active", true); set.insert("active", true);
channels update_channels_last_message(&channels, id, &set).await;
.update_one( },
doc! { "_id": id }, Channel::Group { id, .. } | Channel::TextChannel { id, .. } => {
doc! { update_channels_last_message(&channels, id, &set).await;
"$set": set
},
None,
)
.await
/*.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;*/
.unwrap();
}
Channel::Group { id, .. } => {
if let Content::Text(_) = &ss.content {
channels
.update_one(
doc! { "_id": id },
doc! {
"$set": set
},
None,
)
.await
/*.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;*/
.unwrap();
}
}
Channel::TextChannel { id, .. } => {
if let Content::Text(_) = &ss.content {
channels
.update_one(
doc! { "_id": id },
doc! {
"$set": {
"last_message": &ss.id
}
},
None,
)
.await
/*.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;*/
.unwrap();
}
} }
_ => {} _ => {}
} }
@@ -335,60 +275,47 @@ impl Message {
} }
// Fetch their corresponding sessions. // Fetch their corresponding sessions.
if let Ok(mut cursor) = get_collection("accounts") if target_ids.len() > 0 {
.find( if let Ok(mut cursor) = Session::find(
doc! { &get_db(),
"_id": { doc! {
"$in": target_ids "_id": {
}, "$in": target_ids
"sessions.subscription": { },
"$exists": true "subscription": {
} "$exists": true
},
FindOptions::builder()
.projection(doc! { "sessions": 1 })
.build(),
)
.await
{
let mut subscriptions = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
if let Ok(sessions) = doc.get_array("sessions") {
for session in sessions {
if let Some(doc) = session.as_document() {
if let Ok(sub) = doc.get_document("subscription") {
let endpoint = sub.get_str("endpoint").unwrap().to_string();
let p256dh = sub.get_str("p256dh").unwrap().to_string();
let auth = sub.get_str("auth").unwrap().to_string();
subscriptions
.push(SubscriptionInfo::new(endpoint, p256dh, auth));
}
}
} }
} },
} None
} )
.await {
if subscriptions.len() > 0 {
let enc = serde_json::to_string(&PushNotification::new(self, &c_clone).await).unwrap(); let enc = serde_json::to_string(&PushNotification::new(self, &c_clone).await).unwrap();
let client = WebPushClient::new(); let client = WebPushClient::new();
let key = let key =
base64::decode_config(VAPID_PRIVATE_KEY.clone(), base64::URL_SAFE).unwrap(); base64::decode_config(VAPID_PRIVATE_KEY.clone(), base64::URL_SAFE).unwrap();
for subscription in subscriptions { while let Some(Ok(session)) = cursor.next().await {
let mut builder = WebPushMessageBuilder::new(&subscription).unwrap(); if let Some(sub) = session.subscription {
let sig_builder = VapidSignatureBuilder::from_pem( let subscription = SubscriptionInfo {
std::io::Cursor::new(&key), endpoint: sub.endpoint,
&subscription, keys: SubscriptionKeys {
) auth: sub.auth,
.unwrap(); p256dh: sub.p256dh
let signature = sig_builder.build().unwrap(); }
builder.set_vapid_signature(signature); };
builder.set_payload(ContentEncoding::AesGcm, enc.as_bytes());
let m = builder.build().unwrap(); let mut builder = WebPushMessageBuilder::new(&subscription).unwrap();
client.send(m).await.ok(); let sig_builder = VapidSignatureBuilder::from_pem(
std::io::Cursor::new(&key),
&subscription,
)
.unwrap();
let signature = sig_builder.build().unwrap();
builder.set_vapid_signature(signature);
builder.set_payload(ContentEncoding::AesGcm, enc.as_bytes());
let m = builder.build().unwrap();
client.send(m).await.ok();
}
} }
} }
} }
@@ -482,7 +409,7 @@ impl Message {
let attachment_ids: Vec<String> = let attachment_ids: Vec<String> =
attachments.iter().map(|f| f.id.to_string()).collect(); attachments.iter().map(|f| f.id.to_string()).collect();
get_collection("attachments") get_collection("attachments")
.update_one( .update_many(
doc! { doc! {
"_id": { "_id": {
"$in": attachment_ids "$in": attachment_ids
@@ -497,7 +424,7 @@ impl Message {
) )
.await .await
.map_err(|_| Error::DatabaseError { .map_err(|_| Error::DatabaseError {
operation: "update_one", operation: "update_many",
with: "attachment", with: "attachment",
})?; })?;
} }
@@ -505,3 +432,15 @@ impl Message {
Ok(()) Ok(())
} }
} }
async fn update_channels_last_message(channels: &Collection, channel_id: &String, set: &Document) {
channels
.update_one(
doc! { "_id": channel_id },
doc! { "$set": set },
None,
)
.await
.expect("Server should not run with no, or a corrupted db");
}

View File

@@ -1,6 +1,7 @@
use crate::util::{ use crate::util::{
result::{Error, Result}, result::{Error, Result},
variables::JANUARY_URL, variables::JANUARY_URL,
variables::MAX_EMBED_COUNT,
}; };
use linkify::{LinkFinder, LinkKind}; use linkify::{LinkFinder, LinkKind};
use regex::Regex; use regex::Regex;
@@ -46,6 +47,9 @@ pub enum Special {
None, None,
YouTube { YouTube {
id: String, id: String,
#[serde(skip_serializing_if = "Option::is_none")]
timestamp: Option<String>,
}, },
Twitch { Twitch {
content_type: TwitchType, content_type: TwitchType,
@@ -100,11 +104,15 @@ impl Embed {
pub async fn generate(content: String) -> Result<Vec<Embed>> { pub async fn generate(content: String) -> Result<Vec<Embed>> {
lazy_static! { lazy_static! {
static ref RE_CODE: Regex = Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap(); 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, "");
// Ignore all content between angle brackets starting with http.
let content = RE_IGNORED.replace_all(&content, "");
let content = content let content = content
// Ignore quoted lines. // Ignore quoted lines.
.split("\n") .split("\n")
@@ -120,35 +128,55 @@ impl Embed {
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.join("\n"); .join("\n");
// ! FIXME: allow multiple links
// ! FIXME: prevent generation if link is surrounded with < >
let mut finder = LinkFinder::new(); let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]); finder.kinds(&[LinkKind::Url]);
let links: Vec<_> = finder.links(&content).collect(); let links: Vec<_> = finder.links(&content).take(*MAX_EMBED_COUNT).collect();
if links.len() == 0 { if links.len() == 0 {
return Err(Error::LabelMe); return Err(Error::LabelMe);
} }
let link = &links[0]; let mut embeds: Vec<Embed> = Vec::new();
let client = reqwest::Client::new(); let mut link_index = 0;
let result = client
.get(&format!("{}/embed", *JANUARY_URL))
.query(&[("url", link.as_str())])
.send()
.await;
match result { // ! FIXME: batch request to january?
Err(_) => return Err(Error::LabelMe), while link_index < links.len() {
Ok(result) => match result.status() { let link = &links[link_index];
reqwest::StatusCode::OK => {
let res: Embed = result.json().await.map_err(|_| Error::InvalidOperation)?;
Ok(vec![res]) // Check if we already processed this link.
} if link_index != 0 && links.iter().take(link_index).any(|x| x.as_str() == link.as_str()) {
_ => return Err(Error::LabelMe), link_index = link_index + 1;
}, continue;
}
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/embed", *JANUARY_URL))
.query(&[("url", link.as_str())])
.send()
.await;
if result.is_err() {
link_index = link_index + 1;
continue;
}
let response = result.unwrap();
if response.status().is_success() {
let res: Embed = response.json().await.map_err(|_| Error::InvalidOperation)?;
embeds.push(res);
}
link_index = link_index + 1;
}
// Prevent database update when no embeds are found.
if embeds.len() > 0 {
Ok(embeds)
} else {
Err(Error::LabelMe)
} }
} }
} }

View File

@@ -92,7 +92,7 @@ pub struct Server {
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
pub channels: Vec<String>, pub channels: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<Category>>, pub categories: Option<Vec<Category>>,
@@ -108,6 +108,9 @@ 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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
#[serde(skip_serializing_if = "if_false", default)] #[serde(skip_serializing_if = "if_false", default)]
pub nsfw: bool pub nsfw: bool
} }

View File

@@ -16,6 +16,7 @@ use crate::database::*;
use crate::notifications::websocket::is_online; use crate::notifications::websocket::is_online;
use crate::util::result::{Error, Result}; use crate::util::result::{Error, Result};
use crate::util::variables::EARLY_ADOPTER_BADGE; use crate::util::variables::EARLY_ADOPTER_BADGE;
use crate::util::variables::MAX_SERVER_COUNT;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus { pub enum RelationshipStatus {
@@ -323,4 +324,10 @@ impl User {
.collect::<Vec<Document>>() .collect::<Vec<Document>>()
.await) .await)
} }
/// Check if this user can acquire another server.
pub async fn can_acquire_server(id: &str) -> Result<bool> {
let server_ids = User::fetch_server_ids(&id).await?;
Ok(server_ids.len() < *MAX_SERVER_COUNT)
}
} }

View File

@@ -1,7 +1,7 @@
use crate::database::*; use crate::database::*;
use mongodb::bson::{doc, from_document}; use mongodb::bson::{doc, from_document};
use rauth::auth::Session; use rauth::entities::Session;
use rocket::http::Status; use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request}; use rocket::request::{self, FromRequest, Outcome, Request};
@@ -15,7 +15,7 @@ impl<'r> FromRequest<'r> for User {
.get("x-bot-token") .get("x-bot-token")
.next() .next()
.map(|x| x.to_string()); .map(|x| x.to_string());
if let Some(bot_token) = header_bot_token { if let Some(bot_token) = header_bot_token {
return if let Ok(result) = get_collection("bots") return if let Ok(result) = get_collection("bots")
.find_one( .find_one(
@@ -40,7 +40,10 @@ impl<'r> FromRequest<'r> for User {
if let Some(doc) = result { if let Some(doc) = result {
Outcome::Success(from_document(doc).unwrap()) Outcome::Success(from_document(doc).unwrap())
} else { } else {
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession)) Outcome::Failure((
Status::Forbidden,
rauth::util::Error::InvalidSession,
))
} }
} else { } else {
Outcome::Failure(( Outcome::Failure((
@@ -62,32 +65,34 @@ impl<'r> FromRequest<'r> for User {
with: "bot", with: "bot",
}, },
)) ))
} };
} else { } else {
let session: Session = request.guard::<Session>().await.unwrap(); if let Outcome::Success(session) = request.guard::<Session>().await {
if let Ok(result) = get_collection("users")
if let Ok(result) = get_collection("users") .find_one(
.find_one( doc! {
doc! { "_id": &session.user_id
"_id": &session.user_id },
}, None,
None, )
) .await
.await {
{ if let Some(doc) = result {
if let Some(doc) = result { Outcome::Success(from_document(doc).unwrap())
Outcome::Success(from_document(doc).unwrap()) } else {
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
}
} else { } else {
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession)) Outcome::Failure((
Status::InternalServerError,
rauth::util::Error::DatabaseError {
operation: "find_one",
with: "user",
},
))
} }
} else { } else {
Outcome::Failure(( Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
Status::InternalServerError,
rauth::util::Error::DatabaseError {
operation: "find_one",
with: "user",
},
))
} }
} }
} }

View File

@@ -71,39 +71,6 @@ pub async fn create_database() {
.await .await
.expect("Failed to create pubsub collection."); .expect("Failed to create pubsub collection.");
db.run_command(
doc! {
"createIndexes": "accounts",
"indexes": [
{
"key": {
"email": 1
},
"name": "email",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
},
{
"key": {
"email_normalised": 1
},
"name": "email_normalised",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
}
]
},
None,
)
.await
.expect("Failed to create account index.");
db.run_command( db.run_command(
doc! { doc! {
"createIndexes": "users", "createIndexes": "users",

View File

@@ -10,10 +10,13 @@ pub async fn run_migrations() {
.list_database_names(None, None) .list_database_names(None, None)
.await .await
.expect("Failed to fetch database names."); .expect("Failed to fetch database names.");
if list.iter().position(|x| x == "revolt").is_none() { if list.iter().position(|x| x == "revolt").is_none() {
init::create_database().await; init::create_database().await;
} else { } else {
scripts::migrate_database().await; scripts::migrate_database().await;
} }
// panic!("https://pbs.twimg.com/media/EDTpB5JWwAUvyxd.jpg");
rauth::entities::sync_models(&super::get_db()).await;
} }

View File

@@ -2,7 +2,7 @@ use crate::database::{permissions, get_collection, get_db, PermissionTuple};
use futures::StreamExt; use futures::StreamExt;
use log::info; use log::info;
use mongodb::{bson::{doc, from_document, to_document}, options::FindOptions}; use mongodb::{bson::{Document, doc, from_bson, from_document, to_document}, options::FindOptions};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -11,7 +11,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 8; pub const LATEST_REVISION: i32 = 10;
pub async fn migrate_database() { pub async fn migrate_database() {
let migrations = get_collection("migrations"); let migrations = get_collection("migrations");
@@ -212,6 +212,138 @@ pub async fn run_migrations(revision: i32) -> i32 {
.expect("Failed to create bots collection."); .expect("Failed to create bots collection.");
} }
if revision <= 8 {
info!("Running migration [revision 8 / 2021-09-10]: Update to rAuth version 1.");
get_db()
.run_command(
doc! {
"dropIndexes": "accounts",
"index": ["email", "email_normalised"]
},
None,
)
.await
.expect("Failed to delete legacy account indexes.");
let col = get_collection("sessions");
let mut cursor = get_collection("accounts")
.find(doc! { }, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(account) = doc {
let id = account.get_str("_id").unwrap();
if let Some(sessions) = account.get("sessions") {
#[derive(Deserialize)]
struct Session {
id: String,
token: String,
friendly_name: String,
subscription: Option<Document>,
}
let sessions = from_bson::<Vec<Session>>(sessions.clone()).unwrap();
for session in sessions {
info!("Converting session {} to new format.", &session.id);
let mut doc = doc! {
"_id": session.id,
"token": session.token,
"user_id": id.clone(),
"name": session.friendly_name,
};
if let Some(sub) = session.subscription {
doc.insert("subscription", sub);
}
col.insert_one(doc, None).await.ok();
}
} else {
info!("Account doesn't have any sessions!");
}
}
}
get_collection("accounts")
.update_many(
doc! { },
doc! {
"$unset": {
"sessions": 1,
},
"$set": {
"mfa": {
"recovery_codes": []
}
}
},
None
)
.await
.unwrap();
}
if revision <= 9 {
info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id.");
let mut cursor = get_collection("channels")
.find(doc! { }, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(channel) = doc {
let channel_id = channel.get_str("_id").unwrap();
if let Some(last_message) = channel.get("last_message") {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Obj {
#[serde(rename = "_id")]
id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum LastMessage {
Obj(Obj),
Id(String)
}
let lm = from_bson::<LastMessage>(last_message.clone()).unwrap();
let id = match lm {
LastMessage::Obj(Obj { id }) => id,
LastMessage::Id(id) => id
};
info!("Converting session {} to new format.", &channel_id);
get_collection("channels")
.update_one(
doc! {
"_id": channel_id
},
doc! {
"$set": {
"last_message_id": id
},
"$unset": {
"last_message": 1,
}
},
None
)
.await
.unwrap();
} else {
info!("{} has no last_message.", &channel_id);
}
}
}
}
// 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.
LATEST_REVISION LATEST_REVISION
} }

View File

@@ -21,20 +21,18 @@ pub mod util;
pub mod version; pub mod version;
use async_std::task; use async_std::task;
use chrono::Duration;
use futures::join; use futures::join;
use log::info; use log::info;
use rauth::options::{EmailVerification, Options, SMTP};
use rauth::{ use rauth::{
auth::Auth, config::{Captcha, Config, EmailVerification, SMTPSettings, Template, Templates},
options::{Template, Templates}, logic::Auth,
}; };
use std::str::FromStr;
use rocket_cors::AllowedOrigins;
use rocket::catchers; use rocket::catchers;
use rocket_cors::AllowedOrigins;
use std::str::FromStr;
use util::variables::{ use util::variables::{
APP_URL, HCAPTCHA_KEY, INVITE_ONLY, PUBLIC_URL, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, APP_URL, HCAPTCHA_KEY, INVITE_ONLY, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_USERNAME,
SMTP_USERNAME, USE_EMAIL, USE_HCAPTCHA, USE_EMAIL, USE_HCAPTCHA,
}; };
#[async_std::main] #[async_std::main]
@@ -71,79 +69,67 @@ async fn launch_web() {
let cors = rocket_cors::CorsOptions { let cors = rocket_cors::CorsOptions {
allowed_origins: AllowedOrigins::All, allowed_origins: AllowedOrigins::All,
allowed_methods: [ allowed_methods: [
"Get", "Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch",
"Put",
"Post",
"Delete",
"Options",
"Head",
"Trace",
"Connect",
"Patch",
] ]
.iter() .iter()
.map(|s| FromStr::from_str(s).unwrap()) .map(|s| FromStr::from_str(s).unwrap())
.collect(), .collect(),
..Default::default() ..Default::default()
} }
.to_cors() .to_cors()
.expect("Failed to create CORS."); .expect("Failed to create CORS.");
let mut options = Options::new() let mut config = Config {
.base_url(format!("{}/auth", *PUBLIC_URL)) email_verification: if *USE_EMAIL {
.email_verification(if *USE_EMAIL {
EmailVerification::Enabled { EmailVerification::Enabled {
success_redirect_uri: format!("{}/login", *APP_URL), smtp: SMTPSettings {
welcome_redirect_uri: format!("{}/welcome", *APP_URL),
password_reset_url: Some(format!("{}/login/reset", *APP_URL)),
verification_expiry: Duration::days(1),
password_reset_expiry: Duration::hours(1),
templates: Templates {
verify_email: Template {
title: "Verify your Revolt account.",
text: "You're almost there!
If you did not perform this action you can safely ignore this email.
Please verify your account here: {{url}}",
html: None,
},
reset_password: Template {
title: "Reset your Revolt password.",
text: "You requested a password reset, if you did not perform this action you can safely ignore this email.
Reset your password here: {{url}}",
html: None,
},
welcome: None,
},
smtp: SMTP {
from: (*SMTP_FROM).to_string(), from: (*SMTP_FROM).to_string(),
host: (*SMTP_HOST).to_string(), host: (*SMTP_HOST).to_string(),
username: (*SMTP_USERNAME).to_string(), username: (*SMTP_USERNAME).to_string(),
password: (*SMTP_PASSWORD).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!("../assets/templates/verify.txt").into(),
url: format!("{}/login/verify/", *APP_URL),
html: None,
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("../assets/templates/reset.txt").into(),
url: format!("{}/login/reset/", *APP_URL),
html: None,
},
welcome: None,
}, },
} }
} else { } else {
EmailVerification::Disabled EmailVerification::Disabled
}); },
..Default::default()
};
if *INVITE_ONLY { if *INVITE_ONLY {
options = options.invite_only_collection(database::get_collection("invites")) config.invite_only = true;
} }
if *USE_HCAPTCHA { if *USE_HCAPTCHA {
options = options.hcaptcha_secret(HCAPTCHA_KEY.clone()); config.captcha = Captcha::HCaptcha {
secret: HCAPTCHA_KEY.clone(),
};
} }
let auth = Auth::new(database::get_collection("accounts"), options); let auth = Auth::new(database::get_db(), config);
let rocket = rocket::build(); let rocket = rocket::build();
routes::mount(rocket) routes::mount(rocket)
.mount("/", rocket_cors::catch_all_options_routes()) .mount("/", rocket_cors::catch_all_options_routes())
.mount("/auth", rauth::routes::routes()) .mount("/auth/account", rauth::web::account::routes())
.mount("/auth/session", rauth::web::session::routes())
.manage(auth) .manage(auth)
.manage(cors.clone()) .manage(cors.clone())
.attach(cors) .attach(cors)

View File

@@ -1,6 +1,5 @@
use hive_pubsub::PubSub; use hive_pubsub::PubSub;
use mongodb::bson::doc; use mongodb::bson::doc;
use rauth::auth::Session;
use rocket::serde::json::Value; use rocket::serde::json::Value;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -19,24 +18,24 @@ pub enum WebSocketError {
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct BotAuth { pub struct Auth {
pub token: String pub token: String,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)] #[serde(untagged)]
pub enum AuthType { pub enum Ping {
User(Session), Binary(Vec<u8>),
Bot(BotAuth) Number(usize)
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ServerboundNotification { pub enum ServerboundNotification {
Authenticate(AuthType), Authenticate(Auth),
BeginTyping { channel: String }, BeginTyping { channel: String },
EndTyping { channel: String }, EndTyping { channel: String },
Ping { data: Vec<u8> } Ping { data: Ping, responded: Option<()> },
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@@ -50,7 +49,7 @@ pub enum RemoveUserField {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveChannelField { pub enum RemoveChannelField {
Icon, Icon,
Description Description,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@@ -85,8 +84,9 @@ pub enum ClientboundNotification {
users: Vec<User>, users: Vec<User>,
servers: Vec<Server>, servers: Vec<Server>,
channels: Vec<Channel>, channels: Vec<Channel>,
members: Vec<Member> members: Vec<Member>,
}, },
Pong { data: Ping },
Message(Message), Message(Message),
MessageUpdate { MessageUpdate {
@@ -159,11 +159,11 @@ pub enum ClientboundNotification {
role_id: String, role_id: String,
data: Value, data: Value,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveRoleField> clear: Option<RemoveRoleField>,
}, },
ServerRoleDelete { ServerRoleDelete {
id: String, id: String,
role_id: String role_id: String,
}, },
UserUpdate { UserUpdate {
@@ -222,8 +222,7 @@ pub async fn prehandle_hook(notification: &ClientboundNotification) -> Result<()
subscribe_if_exists(recipient.clone(), channel_id.to_string()).ok(); subscribe_if_exists(recipient.clone(), channel_id.to_string()).ok();
} }
} }
Channel::TextChannel { server, .. } Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
| Channel::VoiceChannel { server, .. } => {
// ! FIXME: write a better algorithm? // ! FIXME: write a better algorithm?
let members = Server::fetch_member_ids(server).await?; let members = Server::fetch_member_ids(server).await?;
for member in members { for member in members {

View File

@@ -1,28 +1,28 @@
use crate::database::*; use crate::database::*;
use crate::notifications::events::{AuthType, BotAuth}; use crate::notifications::events::Ping;
use crate::util::variables::WS_HOST; use crate::util::variables::WS_HOST;
use super::subscriptions; use super::subscriptions;
use async_std::net::{TcpListener, TcpStream}; use async_std::net::{TcpListener, TcpStream};
use async_std::task; use async_std::task;
use async_tungstenite::tungstenite::{Message, handshake::server}; use async_tungstenite::tungstenite::{handshake::server, Message};
use futures::channel::{oneshot, mpsc::{unbounded, UnboundedSender}}; use futures::channel::{
mpsc::{unbounded, UnboundedSender},
oneshot,
};
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
use futures::{pin_mut, prelude::*}; use futures::{pin_mut, prelude::*};
use hive_pubsub::PubSub; use hive_pubsub::PubSub;
use log::{debug, info}; use log::{debug, info};
use many_to_many::ManyToMany; use many_to_many::ManyToMany;
use mongodb::bson::doc; use mongodb::bson::doc;
use rauth::{ use rauth::entities::{Model, Session};
auth::{Auth},
options::Options,
};
use rmp_serde; use rmp_serde;
use url::Url;
use std::collections::HashMap; use std::collections::HashMap;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex, RwLock};
use url::Url;
use super::{ use super::{
events::{ClientboundNotification, ServerboundNotification, WebSocketError}, events::{ClientboundNotification, ServerboundNotification, WebSocketError},
@@ -51,28 +51,43 @@ pub async fn launch_server() {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum MSGFormat { enum MSGFormat {
JSON, JSON,
MSGPACK MSGPACK,
} }
struct HeaderCallback { struct HeaderCallback {
sender: oneshot::Sender<MSGFormat> sender: oneshot::Sender<MSGFormat>,
} }
impl server::Callback for HeaderCallback { impl server::Callback for HeaderCallback {
fn on_request(self, request: &server::Request, response: server::Response) -> Result<server::Response, server::ErrorResponse> { fn on_request(
self,
request: &server::Request,
response: server::Response,
) -> Result<server::Response, server::ErrorResponse> {
// we dont get some of the data sometimes so im generating a fake url with the only data we actually need // we dont get some of the data sometimes so im generating a fake url with the only data we actually need
let url = format!("ws://example.com?{}", request.uri().query().unwrap_or("?format=json")); let url = format!(
let mut query: HashMap<_, _> = url.parse::<Url>().unwrap().query_pairs().into_owned().collect(); // should be safe to use unwrap here as we just made the url ourself "ws://example.com?{}",
request.uri().query().unwrap_or("?format=json")
);
let mut query: HashMap<_, _> = url
.parse::<Url>()
.unwrap()
.query_pairs()
.into_owned()
.collect(); // should be safe to use unwrap here as we just made the url ourself
let format_query: Option<String> = query.remove("format"); let format_query: Option<String> = query.remove("format");
let format = match format_query.as_deref().unwrap_or("json") { let format = match format_query.as_deref().unwrap_or("json") {
"msgpack" => MSGFormat::MSGPACK, "msgpack" => MSGFormat::MSGPACK,
"json" => MSGFormat::JSON, "json" => MSGFormat::JSON,
_ => panic!("unknown format") // TODO: not use panic _ => MSGFormat::JSON, // Fallback to JSON.
}; };
self.sender.send(format).unwrap(); // TODO: not use unwrap if self.sender.send(format).is_ok() {
Ok(response) Ok(response)
} else {
Err(server::ErrorResponse::new(None))
}
} }
} }
@@ -81,12 +96,13 @@ async fn accept(stream: TcpStream) {
.peer_addr() .peer_addr()
.expect("Connected streams should have a peer address."); .expect("Connected streams should have a peer address.");
let (sender, receiver) = oneshot::channel::<MSGFormat>(); let (sender, receiver) = oneshot::channel::<MSGFormat>();
let ws_stream = async_tungstenite::accept_hdr_async_with_config(stream, HeaderCallback { sender }, None)
.await
.expect("Error during websocket handshake.");
let msg_format = receiver.await.unwrap(); // TODO: not use unwrap let ws_stream =
async_tungstenite::accept_hdr_async_with_config(stream, HeaderCallback { sender }, None)
.await
.expect("Error during websocket handshake.");
let msg_format = receiver.await.unwrap(); // TODO: not use unwrap
info!("User established WebSocket connection from {}.", &addr); info!("User established WebSocket connection from {}.", &addr);
@@ -102,8 +118,8 @@ async fn accept(stream: TcpStream) {
} }
MSGFormat::MSGPACK => match rmp_serde::to_vec_named(&notification) { MSGFormat::MSGPACK => match rmp_serde::to_vec_named(&notification) {
Ok(v) => Message::Binary(v), Ok(v) => Message::Binary(v),
Err(_) => return Err(_) => return,
} },
}; };
if let Err(_) = tx.unbounded_send(res) { if let Err(_) = tx.unbounded_send(res) {
@@ -118,21 +134,27 @@ async fn accept(stream: TcpStream) {
let mutex = mutex_generator(); let mutex = mutex_generator();
let maybe_decoded = match msg { let maybe_decoded = match msg {
Message::Text(text) => serde_json::from_str::<ServerboundNotification>(&text).map_err(|e| e.to_string()), Message::Text(text) => {
Message::Binary(vec) => rmp_serde::decode::from_read::<&[u8], ServerboundNotification>(vec.as_slice()).map_err(|e| e.to_string()), serde_json::from_str::<ServerboundNotification>(&text).map_err(|e| e.to_string())
Message::Ping(vec) => Ok(ServerboundNotification::Ping { data: vec }), }
_ => return Ok(()) Message::Binary(vec) => {
rmp_serde::decode::from_read::<&[u8], ServerboundNotification>(vec.as_slice())
.map_err(|e| e.to_string())
}
Message::Ping(vec) => Ok(ServerboundNotification::Ping { data: Ping::Binary(vec), responded: Some(()) }),
_ => return Ok(()),
}; };
let notification = match maybe_decoded { let notification = match maybe_decoded {
Err(why) => { Err(why) => {
send(ClientboundNotification::Error( send(ClientboundNotification::Error(
WebSocketError::MalformedData { WebSocketError::MalformedData {
msg: why.to_string() msg: why.to_string(),
})); },
return Ok(()) ));
}, return Ok(());
Ok(n) => n }
Ok(n) => n,
}; };
match notification { match notification {
@@ -147,34 +169,20 @@ async fn accept(stream: TcpStream) {
} }
} }
if let Some(id) = match auth { let id = if let Ok(Some(session)) =
AuthType::User(new_session) => { Session::find_one(&get_db(), doc! { "token": &auth.token }, None).await
if let Ok(validated_session) = {
Auth::new(get_collection("accounts"), Options::new()) Some(session.user_id)
.verify_session(new_session) } else if let Ok(Some(bot)) = get_collection("bots")
.await .find_one(doc! { "token": auth.token }, None)
{ .await
Some(validated_session.user_id.clone()) {
} else { Some(bot.get_str("_id").unwrap().to_string())
None } else {
} None
} };
AuthType::Bot(BotAuth { token }) => {
if let Ok(doc) = get_collection("bots") if let Some(id) = id {
.find_one(
doc! { "token": token },
None
).await {
if let Some(doc) = doc {
Some(doc.get_str("_id").unwrap().to_string())
} else {
None
}
} else {
None
}
}
} {
if let Ok(user) = (Ref { id: id.clone() }).fetch_user().await { if let Ok(user) = (Ref { id: id.clone() }).fetch_user().await {
let is_invisible = if let Some(status) = &user.status { let is_invisible = if let Some(status) = &user.status {
if let Some(presence) = &status.presence { if let Some(presence) = &status.presence {
@@ -229,7 +237,7 @@ async fn accept(stream: TcpStream) {
data: json!({ data: json!({
"online": true "online": true
}), }),
clear: None clear: None,
} }
.publish_as_user(id); .publish_as_user(id);
} }
@@ -297,8 +305,12 @@ async fn accept(stream: TcpStream) {
return Ok(()); return Ok(());
} }
} }
ServerboundNotification::Ping { data } => { ServerboundNotification::Ping { data, responded } => {
info!("Ping received from User {}. Payload: {:?}", &addr, data); debug!("Ping received from connection {}. Payload: {:?}", &addr, data);
if responded.is_none() {
send(ClientboundNotification::Pong { data });
}
} }
} }
Ok(()) Ok(())
@@ -329,7 +341,7 @@ async fn accept(stream: TcpStream) {
data: json!({ data: json!({
"online": false "online": false
}), }),
clear: None clear: None,
} }
.publish_as_user(id); .publish_as_user(id);
} }

View File

@@ -78,7 +78,7 @@ pub async fn req(user: User, info: Json<Data>) -> Result<Value> {
owner: user.id, owner: user.id,
recipients: set.into_iter().collect::<Vec<String>>(), recipients: set.into_iter().collect::<Vec<String>>(),
icon: None, icon: None,
last_message: None, last_message_id: None,
permissions: None, permissions: None,
nsfw: info.nsfw.unwrap_or_default() nsfw: info.nsfw.unwrap_or_default()
}; };

View File

@@ -1,5 +1,6 @@
use crate::database::*; use crate::database::*;
use crate::util::result::{Error, Result}; use crate::util::result::{Error, Result};
use crate::util::variables::MAX_SERVER_COUNT;
use rocket::serde::json::Value; use rocket::serde::json::Value;
@@ -8,6 +9,12 @@ pub async fn req(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot) return Err(Error::IsBot)
} }
if !User::can_acquire_server(&user.id).await? {
Err(Error::TooManyServers {
max: *MAX_SERVER_COUNT,
})?
}
let target = target.fetch_invite().await?; let target = target.fetch_invite().await?;

View File

@@ -1,7 +1,8 @@
pub use rocket::response::Redirect;
pub use rocket::http::Status; pub use rocket::http::Status;
pub use rocket::response::Redirect;
use rocket::{Build, Rocket}; use rocket::{Build, Rocket};
mod bots;
mod channels; mod channels;
mod invites; mod invites;
mod onboard; mod onboard;
@@ -10,7 +11,6 @@ mod root;
mod servers; mod servers;
mod sync; mod sync;
mod users; mod users;
mod bots;
pub fn mount(rocket: Rocket<Build>) -> Rocket<Build> { pub fn mount(rocket: Rocket<Build>) -> Rocket<Build> {
rocket rocket

View File

@@ -2,7 +2,7 @@ use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse}; use crate::util::result::{Error, Result, EmptyResponse};
use mongodb::bson::doc; use mongodb::bson::doc;
use rauth::auth::Session; use rauth::entities::Session;
use regex::Regex; use regex::Regex;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@@ -1,6 +1,6 @@
use crate::database::*; use crate::database::*;
use rauth::auth::Session; use rauth::entities::Session;
use rocket::serde::json::Value; use rocket::serde::json::Value;
#[get("/hello")] #[get("/hello")]

View File

@@ -1,37 +1,19 @@
use crate::database::*; use crate::database::*;
use crate::util::result::{EmptyResponse, Error, Result}; use crate::util::result::{EmptyResponse, Error, Result};
use mongodb::bson::{doc, to_document}; use mongodb::bson::doc;
use rauth::auth::Session; use rauth::entities::{Model, Session, WebPushSubscription};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Subscription {
endpoint: String,
p256dh: String,
auth: String,
}
#[post("/subscribe", data = "<data>")] #[post("/subscribe", data = "<data>")]
pub async fn req(session: Session, data: Json<Subscription>) -> Result<EmptyResponse> { pub async fn req(mut session: Session, data: Json<WebPushSubscription>) -> Result<EmptyResponse> {
let data = data.into_inner(); session.subscription = Some(data.into_inner());
get_collection("accounts") session
.update_one( .save(&get_db(), None)
doc! {
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$set": {
"sessions.$.subscription": to_document(&data)
.map_err(|_| Error::DatabaseError { operation: "to_document", with: "subscription" })?
}
},
None,
)
.await .await
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "account" })?; .map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
Ok(EmptyResponse {}) operation: "save",
with: "session",
})
} }

View File

@@ -1,29 +1,18 @@
use crate::database::*; use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse}; use crate::util::result::{EmptyResponse, Error, Result};
use mongodb::bson::doc; use mongodb::bson::doc;
use rauth::auth::Session; use rauth::entities::{Model, Session};
#[post("/unsubscribe")] #[post("/unsubscribe")]
pub async fn req(session: Session) -> Result<EmptyResponse> { pub async fn req(mut session: Session) -> Result<EmptyResponse> {
get_collection("accounts") session.subscription = None;
.update_one( session
doc! { .save(&get_db(), None)
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$unset": {
"sessions.$.subscription": 1
}
},
None,
)
.await .await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError { .map_err(|_| Error::DatabaseError {
operation: "to_document", operation: "save",
with: "subscription", with: "session",
})?; })
Ok(EmptyResponse {})
} }

View File

@@ -1,7 +1,11 @@
use crate::util::{ratelimit::RateLimitGuard, variables::{ use crate::util::{
APP_URL, AUTUMN_URL, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, JANUARY_URL, USE_AUTUMN, ratelimit::RateLimitGuard,
USE_EMAIL, USE_HCAPTCHA, USE_JANUARY, USE_VOSO, VAPID_PUBLIC_KEY, VOSO_URL, VOSO_WS_HOST, variables::{
}}; APP_URL, AUTUMN_URL, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, JANUARY_URL,
USE_AUTUMN, USE_EMAIL, USE_HCAPTCHA, USE_JANUARY, USE_VOSO, VAPID_PUBLIC_KEY, VOSO_URL,
VOSO_WS_HOST,
},
};
use mongodb::bson::doc; use mongodb::bson::doc;
use rocket::{http::Status, serde::json::Value}; use rocket::{http::Status, serde::json::Value};

View File

@@ -79,7 +79,7 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<Value> {
name: info.name, name: info.name,
description: info.description, description: info.description,
icon: None, icon: None,
last_message: None, last_message_id: None,
default_permissions: None, default_permissions: None,
role_permissions: HashMap::new(), role_permissions: HashMap::new(),

View File

@@ -2,6 +2,7 @@ use std::collections::HashMap;
use crate::database::*; use crate::database::*;
use crate::util::result::{Error, Result}; use crate::util::result::{Error, Result};
use crate::util::variables::MAX_SERVER_COUNT;
use mongodb::bson::doc; use mongodb::bson::doc;
use rocket::serde::json::{Json, Value}; use rocket::serde::json::{Json, Value};
@@ -27,6 +28,12 @@ pub async fn req(user: User, info: Json<Data>) -> Result<Value> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot) return Err(Error::IsBot)
} }
if !User::can_acquire_server(&user.id).await? {
Err(Error::TooManyServers {
max: *MAX_SERVER_COUNT,
})?
}
let info = info.into_inner(); let info = info.into_inner();
info.validate() info.validate()
@@ -77,6 +84,8 @@ pub async fn req(user: User, info: Json<Data>) -> Result<Value> {
icon: None, icon: None,
banner: None, banner: None,
flags: None,
nsfw: info.nsfw.unwrap_or_default() nsfw: info.nsfw.unwrap_or_default()
}; };
@@ -88,7 +97,7 @@ pub async fn req(user: User, info: Json<Data>) -> Result<Value> {
description: None, description: None,
icon: None, icon: None,
last_message: None, last_message_id: None,
default_permissions: None, default_permissions: None,
role_permissions: HashMap::new(), role_permissions: HashMap::new(),

View File

@@ -14,9 +14,9 @@ pub struct Options {
#[post("/settings/fetch", data = "<options>")] #[post("/settings/fetch", data = "<options>")]
pub async fn req(user: User, options: Json<Options>) -> Result<Value> { pub async fn req(user: User, options: Json<Options>) -> Result<Value> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot) return Err(Error::IsBot);
} }
let options = options.into_inner(); let options = options.into_inner();
let mut projection = doc! { let mut projection = doc! {
"_id": 0, "_id": 0,

View File

@@ -7,8 +7,8 @@ use rocket::serde::json::Value;
#[get("/unreads")] #[get("/unreads")]
pub async fn req(user: User) -> Result<Value> { pub async fn req(user: User) -> Result<Value> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot) return Err(Error::IsBot);
} }
Ok(json!(User::fetch_unreads(&user.id).await?)) Ok(json!(User::fetch_unreads(&user.id).await?))
} }

View File

@@ -1,6 +1,6 @@
use crate::database::*; use crate::database::*;
use crate::notifications::events::ClientboundNotification; use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse}; use crate::util::result::{EmptyResponse, Error, Result};
use chrono::prelude::*; use chrono::prelude::*;
use mongodb::bson::{doc, to_bson}; use mongodb::bson::{doc, to_bson};
@@ -20,7 +20,7 @@ pub struct Options {
#[post("/settings/set?<options..>", data = "<data>")] #[post("/settings/set?<options..>", data = "<data>")]
pub async fn req(user: User, data: Json<Data>, options: Options) -> Result<EmptyResponse> { pub async fn req(user: User, data: Json<Data>, options: Options) -> Result<EmptyResponse> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot) return Err(Error::IsBot);
} }
let data = data.into_inner(); let data = data.into_inner();

View File

@@ -3,9 +3,8 @@ use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse}; use crate::util::result::{Error, Result, EmptyResponse};
use mongodb::bson::doc; use mongodb::bson::doc;
use rauth::auth::{Auth, Session}; use rauth::entities::Account;
use regex::Regex; use regex::Regex;
use rocket::State;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
@@ -26,8 +25,7 @@ pub struct Data {
#[patch("/<_ignore_id>/username", data = "<data>")] #[patch("/<_ignore_id>/username", data = "<data>")]
pub async fn req( pub async fn req(
auth: &State<Auth>, account: Account,
session: Session,
user: User, user: User,
data: Json<Data>, data: Json<Data>,
_ignore_id: String, _ignore_id: String,
@@ -39,8 +37,7 @@ pub async fn req(
data.validate() data.validate()
.map_err(|error| Error::FailedValidation { error })?; .map_err(|error| Error::FailedValidation { error })?;
auth.verify_password(&session, data.password.clone()) account.verify_password(&data.password)
.await
.map_err(|_| Error::InvalidCredentials)?; .map_err(|_| Error::InvalidCredentials)?;
let mut set = doc! {}; let mut set = doc! {};

9
src/routes/users/fetch_self.rs Executable file
View File

@@ -0,0 +1,9 @@
use crate::database::*;
use crate::util::result::{Result};
use rocket::serde::json::Value;
#[get("/@me")]
pub async fn req(user: User) -> Result<Value> {
Ok(json!(user))
}

View File

@@ -14,10 +14,12 @@ mod get_default_avatar;
mod open_dm; mod open_dm;
mod remove_friend; mod remove_friend;
mod unblock_user; mod unblock_user;
mod fetch_self;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![
// User Information // User Information
fetch_self::req,
fetch_user::req, fetch_user::req,
edit_user::req, edit_user::req,
change_username::req, change_username::req,

View File

@@ -40,7 +40,7 @@ pub async fn req(user: User, target: Ref) -> Result<Value> {
id, id,
active: false, active: false,
recipients: vec![user.id, target.id], recipients: vec![user.id, target.id],
last_message: None, last_message_id: None,
} }
}; };

View File

@@ -43,6 +43,9 @@ pub enum Error {
UnknownServer, UnknownServer,
InvalidRole, InvalidRole,
Banned, Banned,
TooManyServers{
max: usize,
},
// ? Bot related errors. // ? Bot related errors.
ReachedMaximumBots, ReachedMaximumBots,
@@ -111,6 +114,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::UnknownServer => Status::NotFound, Error::UnknownServer => Status::NotFound,
Error::InvalidRole => Status::NotFound, Error::InvalidRole => Status::NotFound,
Error::Banned => Status::Forbidden, Error::Banned => Status::Forbidden,
Error::TooManyServers { .. } => Status::Forbidden,
Error::ReachedMaximumBots => Status::BadRequest, Error::ReachedMaximumBots => Status::BadRequest,
Error::IsBot => Status::BadRequest, Error::IsBot => Status::BadRequest,

View File

@@ -64,6 +64,10 @@ lazy_static! {
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 ref MAX_BOT_COUNT: usize = pub static ref MAX_BOT_COUNT: usize =
env::var("REVOLT_MAX_BOT_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 ref MAX_EMBED_COUNT: usize =
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 = pub static ref EARLY_ADOPTER_BADGE: i64 =
env::var("REVOLT_EARLY_ADOPTER_BADGE").unwrap_or_else(|_| "0".to_string()).parse().unwrap(); env::var("REVOLT_EARLY_ADOPTER_BADGE").unwrap_or_else(|_| "0".to_string()).parse().unwrap();
} }

View File

@@ -1 +1 @@
pub const VERSION: &str = "0.5.2-alpha.2"; pub const VERSION: &str = "0.5.3-alpha.1";