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

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

View File

@@ -8,15 +8,13 @@ use crate::{
use futures::StreamExt;
use mongodb::options::UpdateOptions;
use mongodb::{
bson::{doc, to_bson, DateTime},
options::FindOptions,
bson::{doc, to_bson, DateTime, Document},
};
use rauth::entities::{Model, Session};
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use web_push::{
ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder,
};
use web_push::{ContentEncoding, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder};
use std::time::SystemTime;
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -173,7 +171,6 @@ impl Message {
nonce: None,
channel,
author,
content,
attachments: None,
edited: None,
@@ -198,76 +195,19 @@ impl Message {
let ss = self.clone();
let c_clone = channel.clone();
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
// ! FIXME: temp code
let last_message_id = ss.id.clone();
let mut set = doc! { "last_message_id": last_message_id };
let channels = get_collection("channels");
match &c_clone {
Channel::DirectMessage { id, .. } => {
// ! MARK AS ACTIVE
set.insert("active", true);
channels
.update_one(
doc! { "_id": id },
doc! {
"$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();
}
update_channels_last_message(&channels, id, &set).await;
},
Channel::Group { id, .. } | Channel::TextChannel { id, .. } => {
update_channels_last_message(&channels, id, &set).await;
}
_ => {}
}
@@ -335,60 +275,47 @@ impl Message {
}
// Fetch their corresponding sessions.
if let Ok(mut cursor) = get_collection("accounts")
.find(
doc! {
"_id": {
"$in": target_ids
},
"sessions.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));
}
}
if target_ids.len() > 0 {
if let Ok(mut cursor) = Session::find(
&get_db(),
doc! {
"_id": {
"$in": target_ids
},
"subscription": {
"$exists": true
}
}
}
}
if subscriptions.len() > 0 {
},
None
)
.await {
let enc = serde_json::to_string(&PushNotification::new(self, &c_clone).await).unwrap();
let client = WebPushClient::new();
let key =
base64::decode_config(VAPID_PRIVATE_KEY.clone(), base64::URL_SAFE).unwrap();
for subscription in subscriptions {
let mut builder = WebPushMessageBuilder::new(&subscription).unwrap();
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();
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
}
};
let mut builder = WebPushMessageBuilder::new(&subscription).unwrap();
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> =
attachments.iter().map(|f| f.id.to_string()).collect();
get_collection("attachments")
.update_one(
.update_many(
doc! {
"_id": {
"$in": attachment_ids
@@ -497,7 +424,7 @@ impl Message {
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
operation: "update_many",
with: "attachment",
})?;
}
@@ -505,3 +432,15 @@ impl Message {
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::{
result::{Error, Result},
variables::JANUARY_URL,
variables::MAX_EMBED_COUNT,
};
use linkify::{LinkFinder, LinkKind};
use regex::Regex;
@@ -46,6 +47,9 @@ pub enum Special {
None,
YouTube {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
timestamp: Option<String>,
},
Twitch {
content_type: TwitchType,
@@ -100,11 +104,15 @@ impl Embed {
pub async fn generate(content: String) -> Result<Vec<Embed>> {
lazy_static! {
static ref RE_CODE: Regex = Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap();
static ref RE_IGNORED: Regex = Regex::new("(<http.+>)").unwrap();
}
// Ignore code blocks.
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
// Ignore quoted lines.
.split("\n")
@@ -120,35 +128,55 @@ impl Embed {
.collect::<Vec<&str>>()
.join("\n");
// ! FIXME: allow multiple links
// ! FIXME: prevent generation if link is surrounded with < >
let mut finder = LinkFinder::new();
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 {
return Err(Error::LabelMe);
}
let link = &links[0];
let mut embeds: Vec<Embed> = Vec::new();
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/embed", *JANUARY_URL))
.query(&[("url", link.as_str())])
.send()
.await;
let mut link_index = 0;
match result {
Err(_) => return Err(Error::LabelMe),
Ok(result) => match result.status() {
reqwest::StatusCode::OK => {
let res: Embed = result.json().await.map_err(|_| Error::InvalidOperation)?;
// ! FIXME: batch request to january?
while link_index < links.len() {
let link = &links[link_index];
Ok(vec![res])
}
_ => return Err(Error::LabelMe),
},
// Check if we already processed this link.
if link_index != 0 && links.iter().take(link_index).any(|x| x.as_str() == link.as_str()) {
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,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub channels: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<Category>>,
@@ -108,6 +108,9 @@ pub struct Server {
#[serde(skip_serializing_if = "Option::is_none")]
pub banner: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
#[serde(skip_serializing_if = "if_false", default)]
pub nsfw: bool
}

View File

@@ -16,6 +16,7 @@ use crate::database::*;
use crate::notifications::websocket::is_online;
use crate::util::result::{Error, Result};
use crate::util::variables::EARLY_ADOPTER_BADGE;
use crate::util::variables::MAX_SERVER_COUNT;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus {
@@ -323,4 +324,10 @@ impl User {
.collect::<Vec<Document>>()
.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 mongodb::bson::{doc, from_document};
use rauth::auth::Session;
use rauth::entities::Session;
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
@@ -15,7 +15,7 @@ impl<'r> FromRequest<'r> for User {
.get("x-bot-token")
.next()
.map(|x| x.to_string());
if let Some(bot_token) = header_bot_token {
return if let Ok(result) = get_collection("bots")
.find_one(
@@ -40,7 +40,10 @@ impl<'r> FromRequest<'r> for User {
if let Some(doc) = result {
Outcome::Success(from_document(doc).unwrap())
} else {
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
Outcome::Failure((
Status::Forbidden,
rauth::util::Error::InvalidSession,
))
}
} else {
Outcome::Failure((
@@ -62,32 +65,34 @@ impl<'r> FromRequest<'r> for User {
with: "bot",
},
))
}
};
} else {
let session: Session = request.guard::<Session>().await.unwrap();
if let Ok(result) = get_collection("users")
.find_one(
doc! {
"_id": &session.user_id
},
None,
)
.await
{
if let Some(doc) = result {
Outcome::Success(from_document(doc).unwrap())
if let Outcome::Success(session) = request.guard::<Session>().await {
if let Ok(result) = get_collection("users")
.find_one(
doc! {
"_id": &session.user_id
},
None,
)
.await
{
if let Some(doc) = result {
Outcome::Success(from_document(doc).unwrap())
} else {
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
}
} else {
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
Outcome::Failure((
Status::InternalServerError,
rauth::util::Error::DatabaseError {
operation: "find_one",
with: "user",
},
))
}
} else {
Outcome::Failure((
Status::InternalServerError,
rauth::util::Error::DatabaseError {
operation: "find_one",
with: "user",
},
))
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
}
}
}

View File

@@ -71,39 +71,6 @@ pub async fn create_database() {
.await
.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(
doc! {
"createIndexes": "users",

View File

@@ -10,10 +10,13 @@ pub async fn run_migrations() {
.list_database_names(None, None)
.await
.expect("Failed to fetch database names.");
if list.iter().position(|x| x == "revolt").is_none() {
init::create_database().await;
} else {
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 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};
#[derive(Serialize, Deserialize)]
@@ -11,7 +11,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 8;
pub const LATEST_REVISION: i32 = 10;
pub async fn migrate_database() {
let migrations = get_collection("migrations");
@@ -212,6 +212,138 @@ pub async fn run_migrations(revision: i32) -> i32 {
.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.
LATEST_REVISION
}

View File

@@ -21,20 +21,18 @@ pub mod util;
pub mod version;
use async_std::task;
use chrono::Duration;
use futures::join;
use log::info;
use rauth::options::{EmailVerification, Options, SMTP};
use rauth::{
auth::Auth,
options::{Template, Templates},
config::{Captcha, Config, EmailVerification, SMTPSettings, Template, Templates},
logic::Auth,
};
use std::str::FromStr;
use rocket_cors::AllowedOrigins;
use rocket::catchers;
use rocket_cors::AllowedOrigins;
use std::str::FromStr;
use util::variables::{
APP_URL, HCAPTCHA_KEY, INVITE_ONLY, PUBLIC_URL, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD,
SMTP_USERNAME, USE_EMAIL, USE_HCAPTCHA,
APP_URL, HCAPTCHA_KEY, INVITE_ONLY, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_USERNAME,
USE_EMAIL, USE_HCAPTCHA,
};
#[async_std::main]
@@ -71,79 +69,67 @@ async fn launch_web() {
let cors = rocket_cors::CorsOptions {
allowed_origins: AllowedOrigins::All,
allowed_methods: [
"Get",
"Put",
"Post",
"Delete",
"Options",
"Head",
"Trace",
"Connect",
"Patch",
"Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch",
]
.iter()
.map(|s| FromStr::from_str(s).unwrap())
.collect(),
.iter()
.map(|s| FromStr::from_str(s).unwrap())
.collect(),
..Default::default()
}
.to_cors()
.expect("Failed to create CORS.");
let mut options = Options::new()
.base_url(format!("{}/auth", *PUBLIC_URL))
.email_verification(if *USE_EMAIL {
let mut config = Config {
email_verification: if *USE_EMAIL {
EmailVerification::Enabled {
success_redirect_uri: format!("{}/login", *APP_URL),
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 {
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!("../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 {
EmailVerification::Disabled
});
},
..Default::default()
};
if *INVITE_ONLY {
options = options.invite_only_collection(database::get_collection("invites"))
config.invite_only = true;
}
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();
routes::mount(rocket)
.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(cors.clone())
.attach(cors)

View File

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

View File

@@ -1,28 +1,28 @@
use crate::database::*;
use crate::notifications::events::{AuthType, BotAuth};
use crate::notifications::events::Ping;
use crate::util::variables::WS_HOST;
use super::subscriptions;
use async_std::net::{TcpListener, TcpStream};
use async_std::task;
use async_tungstenite::tungstenite::{Message, handshake::server};
use futures::channel::{oneshot, mpsc::{unbounded, UnboundedSender}};
use async_tungstenite::tungstenite::{handshake::server, Message};
use futures::channel::{
mpsc::{unbounded, UnboundedSender},
oneshot,
};
use futures::stream::TryStreamExt;
use futures::{pin_mut, prelude::*};
use hive_pubsub::PubSub;
use log::{debug, info};
use many_to_many::ManyToMany;
use mongodb::bson::doc;
use rauth::{
auth::{Auth},
options::Options,
};
use rauth::entities::{Model, Session};
use rmp_serde;
use url::Url;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex, RwLock};
use url::Url;
use super::{
events::{ClientboundNotification, ServerboundNotification, WebSocketError},
@@ -51,28 +51,43 @@ pub async fn launch_server() {
#[derive(Debug, Clone)]
enum MSGFormat {
JSON,
MSGPACK
MSGPACK,
}
struct HeaderCallback {
sender: oneshot::Sender<MSGFormat>
sender: oneshot::Sender<MSGFormat>,
}
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
let url = format!("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 url = format!(
"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 = match format_query.as_deref().unwrap_or("json") {
"msgpack" => MSGFormat::MSGPACK,
"json" => MSGFormat::JSON,
_ => panic!("unknown format") // TODO: not use panic
_ => MSGFormat::JSON, // Fallback to JSON.
};
self.sender.send(format).unwrap(); // TODO: not use unwrap
Ok(response)
if self.sender.send(format).is_ok() {
Ok(response)
} else {
Err(server::ErrorResponse::new(None))
}
}
}
@@ -81,12 +96,13 @@ async fn accept(stream: TcpStream) {
.peer_addr()
.expect("Connected streams should have a peer address.");
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);
@@ -102,8 +118,8 @@ async fn accept(stream: TcpStream) {
}
MSGFormat::MSGPACK => match rmp_serde::to_vec_named(&notification) {
Ok(v) => Message::Binary(v),
Err(_) => return
}
Err(_) => return,
},
};
if let Err(_) = tx.unbounded_send(res) {
@@ -118,21 +134,27 @@ async fn accept(stream: TcpStream) {
let mutex = mutex_generator();
let maybe_decoded = match msg {
Message::Text(text) => serde_json::from_str::<ServerboundNotification>(&text).map_err(|e| e.to_string()),
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: vec }),
_ => return Ok(())
Message::Text(text) => {
serde_json::from_str::<ServerboundNotification>(&text).map_err(|e| e.to_string())
}
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 {
Err(why) => {
send(ClientboundNotification::Error(
WebSocketError::MalformedData {
msg: why.to_string()
}));
return Ok(())
},
Ok(n) => n
msg: why.to_string(),
},
));
return Ok(());
}
Ok(n) => n,
};
match notification {
@@ -147,34 +169,20 @@ async fn accept(stream: TcpStream) {
}
}
if let Some(id) = match auth {
AuthType::User(new_session) => {
if let Ok(validated_session) =
Auth::new(get_collection("accounts"), Options::new())
.verify_session(new_session)
.await
{
Some(validated_session.user_id.clone())
} else {
None
}
}
AuthType::Bot(BotAuth { token }) => {
if let Ok(doc) = get_collection("bots")
.find_one(
doc! { "token": token },
None
).await {
if let Some(doc) = doc {
Some(doc.get_str("_id").unwrap().to_string())
} else {
None
}
} else {
None
}
}
} {
let id = if let Ok(Some(session)) =
Session::find_one(&get_db(), doc! { "token": &auth.token }, None).await
{
Some(session.user_id)
} else if let Ok(Some(bot)) = get_collection("bots")
.find_one(doc! { "token": auth.token }, None)
.await
{
Some(bot.get_str("_id").unwrap().to_string())
} else {
None
};
if let Some(id) = id {
if let Ok(user) = (Ref { id: id.clone() }).fetch_user().await {
let is_invisible = if let Some(status) = &user.status {
if let Some(presence) = &status.presence {
@@ -229,7 +237,7 @@ async fn accept(stream: TcpStream) {
data: json!({
"online": true
}),
clear: None
clear: None,
}
.publish_as_user(id);
}
@@ -297,8 +305,12 @@ async fn accept(stream: TcpStream) {
return Ok(());
}
}
ServerboundNotification::Ping { data } => {
info!("Ping received from User {}. Payload: {:?}", &addr, data);
ServerboundNotification::Ping { data, responded } => {
debug!("Ping received from connection {}. Payload: {:?}", &addr, data);
if responded.is_none() {
send(ClientboundNotification::Pong { data });
}
}
}
Ok(())
@@ -329,7 +341,7 @@ async fn accept(stream: TcpStream) {
data: json!({
"online": false
}),
clear: None
clear: None,
}
.publish_as_user(id);
}

View File

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

View File

@@ -1,5 +1,6 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_SERVER_COUNT;
use rocket::serde::json::Value;
@@ -8,6 +9,12 @@ pub async fn req(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
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?;

View File

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

View File

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

View File

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

View File

@@ -1,37 +1,19 @@
use crate::database::*;
use crate::util::result::{EmptyResponse, Error, Result};
use mongodb::bson::{doc, to_document};
use rauth::auth::Session;
use mongodb::bson::doc;
use rauth::entities::{Model, Session, WebPushSubscription};
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>")]
pub async fn req(session: Session, data: Json<Subscription>) -> Result<EmptyResponse> {
let data = data.into_inner();
get_collection("accounts")
.update_one(
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,
)
pub async fn req(mut session: Session, data: Json<WebPushSubscription>) -> Result<EmptyResponse> {
session.subscription = Some(data.into_inner());
session
.save(&get_db(), None)
.await
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "account" })?;
Ok(EmptyResponse {})
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
operation: "save",
with: "session",
})
}

View File

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

View File

@@ -1,7 +1,11 @@
use crate::util::{ratelimit::RateLimitGuard, 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 crate::util::{
ratelimit::RateLimitGuard,
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 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,
description: info.description,
icon: None,
last_message: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -43,6 +43,9 @@ pub enum Error {
UnknownServer,
InvalidRole,
Banned,
TooManyServers{
max: usize,
},
// ? Bot related errors.
ReachedMaximumBots,
@@ -111,6 +114,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::UnknownServer => Status::NotFound,
Error::InvalidRole => Status::NotFound,
Error::Banned => Status::Forbidden,
Error::TooManyServers { .. } => Status::Forbidden,
Error::ReachedMaximumBots => 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();
pub static ref MAX_BOT_COUNT: usize =
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 =
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";