Compare commits

...
24 changed files with 174 additions and 64 deletions

No files matched your search

Generated
+1 -1
View File
@@ -2473,7 +2473,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt" name = "revolt"
version = "0.3.3-alpha.6" version = "0.3.3-alpha.7"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt" name = "revolt"
version = "0.3.3-alpha.6" version = "0.3.3-alpha.7"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
+25 -2
View File
@@ -13,7 +13,9 @@ use mongodb::{
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ulid::Ulid; use ulid::Ulid;
use web_push::{ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder}; use web_push::{
ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder,
};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message { pub struct Message {
@@ -171,7 +173,8 @@ impl Message {
for subscription in subscriptions { for subscription in subscriptions {
let mut builder = WebPushMessageBuilder::new(&subscription).unwrap(); let mut builder = WebPushMessageBuilder::new(&subscription).unwrap();
let sig_builder = let sig_builder =
VapidSignatureBuilder::from_pem(std::io::Cursor::new(&key), &subscription).unwrap(); VapidSignatureBuilder::from_pem(std::io::Cursor::new(&key), &subscription)
.unwrap();
let signature = sig_builder.build().unwrap(); let signature = sig_builder.build().unwrap();
builder.set_vapid_signature(signature); builder.set_vapid_signature(signature);
builder.set_payload(ContentEncoding::AesGcm, enc.as_bytes()); builder.set_payload(ContentEncoding::AesGcm, enc.as_bytes());
@@ -198,6 +201,26 @@ impl Message {
} }
pub async fn delete(&self) -> Result<()> { pub async fn delete(&self) -> Result<()> {
if let Some(attachment) = &self.attachment {
get_collection("attachments")
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
"deleted": true
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
}
get_collection("messages") get_collection("messages")
.delete_one( .delete_one(
doc! { doc! {
+34 -1
View File
@@ -1,6 +1,11 @@
use mongodb::bson::doc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use mongodb::options::{Collation, FindOneOptions};
use crate::{database::permissions::user::UserPermissions, notifications::websocket::is_online}; use crate::database::*;
use crate::util::result::{Error, Result};
use crate::notifications::websocket::is_online;
use crate::database::permissions::user::UserPermissions;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus { pub enum RelationshipStatus {
@@ -62,4 +67,32 @@ impl User {
self self
} }
/// Utility function for checking claimed usernames.
pub async fn is_username_taken(username: &str) -> Result<bool> {
if username == "revolt" && username == "admin" {
return Ok(true)
}
if get_collection("users")
.find_one(
doc! {
"username": username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.is_some()
{
Ok(true)
} else {
Ok(false)
}
}
} }
+14
View File
@@ -26,6 +26,12 @@ pub enum WebSocketError {
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ServerboundNotification { pub enum ServerboundNotification {
Authenticate(Session), Authenticate(Session),
BeginTyping {
channel: String
},
EndTyping {
channel: String
}
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@@ -63,6 +69,14 @@ pub enum ClientboundNotification {
ChannelDelete { ChannelDelete {
id: String, id: String,
}, },
ChannelStartTyping {
id: String,
user: String
},
ChannelStopTyping {
id: String,
user: String
},
UserRelationship { UserRelationship {
id: String, id: String,
+48 -2
View File
@@ -71,8 +71,6 @@ async fn accept(stream: TcpStream) {
let fwd = rx.map(Ok).forward(write); let fwd = rx.map(Ok).forward(write);
let incoming = read.try_for_each(async move |msg| { let incoming = read.try_for_each(async move |msg| {
let mutex = mutex_generator(); let mutex = mutex_generator();
//dbg!(&mutex.lock().unwrap());
if let Message::Text(text) = msg { if let Message::Text(text) = msg {
if let Ok(notification) = serde_json::from_str::<ServerboundNotification>(&text) { if let Ok(notification) = serde_json::from_str::<ServerboundNotification>(&text) {
match notification { match notification {
@@ -161,6 +159,54 @@ async fn accept(stream: TcpStream) {
)); ));
} }
} }
// ! TEMP: verify user part of channel
// ! Could just run permission check here.
ServerboundNotification::BeginTyping { channel } => {
if mutex.lock().unwrap().is_some() {
let user = {
let mutex = mutex.lock().unwrap();
let session = mutex.as_ref().unwrap();
session.user_id.clone()
};
ClientboundNotification::ChannelStartTyping {
id: channel.clone(),
user
}
.publish(channel)
.await
.ok();
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
ServerboundNotification::EndTyping { channel } => {
if mutex.lock().unwrap().is_some() {
let user = {
let mutex = mutex.lock().unwrap();
let session = mutex.as_ref().unwrap();
session.user_id.clone()
};
ClientboundNotification::ChannelStopTyping {
id: channel.clone(),
user
}
.publish(channel)
.await
.ok();
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
} }
} }
} }
+3 -2
View File
@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
match &target { match &target {
@@ -102,7 +102,8 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
Message::create( Message::create(
"00000000000000000000000000".to_string(), "00000000000000000000000000".to_string(),
id.clone(), id.clone(),
format!("<@{}> left the group.", user.id), // ! FIXME: make a schema for this
format!("{{\"type\":\"user_left\",\"id\":\"{}\"}}", user.id),
) )
.publish(&target) .publish(&target)
.await .await
+1 -1
View File
@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
Ok(json!(target)) Ok(json!(target))
+3 -2
View File
@@ -16,7 +16,7 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
if let Channel::Group { id, recipients, .. } = &channel { if let Channel::Group { id, recipients, .. } = &channel {
@@ -59,7 +59,8 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
Message::create( Message::create(
"00000000000000000000000000".to_string(), "00000000000000000000000000".to_string(),
id.clone(), id.clone(),
format!("<@{}> added <@{}> to the group.", user.id, member.id), // ! FIXME: make a schema for this
format!("{{\"type\":\"user_added\",\"id\":\"{}\",\"by\":\"{}\"}}", member.id, user.id),
) )
.publish(&channel) .publish(&channel)
.await .await
+3 -2
View File
@@ -20,7 +20,7 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
{ {
if &user.id != owner { if &user.id != owner {
// figure out if we want to use perm system here // figure out if we want to use perm system here
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
if recipients.iter().find(|x| *x == &member.id).is_none() { if recipients.iter().find(|x| *x == &member.id).is_none() {
@@ -56,7 +56,8 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
Message::create( Message::create(
"00000000000000000000000000".to_string(), "00000000000000000000000000".to_string(),
id.clone(), id.clone(),
format!("<@{}> removed <@{}> from the group.", user.id, member.id), // ! FIXME: make a schema for this
format!("{{\"type\":\"user_remove\",\"id\":\"{}\",\"by\":\"{}\"}}", member.id, user.id),
) )
.publish(&channel) .publish(&channel)
.await .await
+1 -1
View File
@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref, msg: Ref) -> Result<()> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
let message = msg.fetch_message(&channel).await?; let message = msg.fetch_message(&channel).await?;
+1 -1
View File
@@ -24,7 +24,7 @@ pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result<
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
let message = msg.fetch_message(&channel).await?; let message = msg.fetch_message(&channel).await?;
+1 -1
View File
@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref, msg: Ref) -> Result<JsonValue> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
let message = msg.fetch_message(&channel).await?; let message = msg.fetch_message(&channel).await?;
+1 -1
View File
@@ -34,7 +34,7 @@ pub async fn req(user: User, target: Ref, options: Form<Options>) -> Result<Json
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
let mut query = doc! { "channel": target.id() }; let mut query = doc! { "channel": target.id() };
+2 -2
View File
@@ -14,7 +14,7 @@ pub struct Options {
#[post("/<target>/messages/stale", data = "<data>")] #[post("/<target>/messages/stale", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<JsonValue> { pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<JsonValue> {
if data.ids.len() > 150 { if data.ids.len() > 150 {
return Err(Error::LabelMe); return Err(Error::TooManyIds);
} }
let target = target.fetch_channel().await?; let target = target.fetch_channel().await?;
@@ -24,7 +24,7 @@ pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<JsonVal
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
let mut cursor = get_collection("messages") let mut cursor = get_collection("messages")
+6 -2
View File
@@ -12,7 +12,7 @@ use validator::Validate;
#[derive(Validate, Serialize, Deserialize)] #[derive(Validate, Serialize, Deserialize)]
pub struct Data { pub struct Data {
#[validate(length(min = 1, max = 2000))] #[validate(length(min = 0, max = 2000))]
content: String, content: String,
// Maximum length of 36 allows both ULIDs and UUIDs. // Maximum length of 36 allows both ULIDs and UUIDs.
#[validate(length(min = 1, max = 36))] #[validate(length(min = 1, max = 36))]
@@ -26,6 +26,10 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
message message
.validate() .validate()
.map_err(|error| Error::FailedValidation { error })?; .map_err(|error| Error::FailedValidation { error })?;
if message.content.len() == 0 && message.attachment.is_none() {
return Err(Error::EmptyMessage);
}
let target = target.fetch_channel().await?; let target = target.fetch_channel().await?;
@@ -34,7 +38,7 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_send_message() { if !perm.get_send_message() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
if get_collection("messages") if get_collection("messages")
+4 -25
View File
@@ -2,7 +2,6 @@ use crate::database::*;
use crate::util::result::{Error, Result}; use crate::util::result::{Error, Result};
use mongodb::bson::doc; use mongodb::bson::doc;
use mongodb::options::{Collation, FindOneOptions};
use rauth::auth::Session; use rauth::auth::Session;
use regex::Regex; use regex::Regex;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
@@ -10,7 +9,7 @@ use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
lazy_static! { lazy_static! {
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap(); static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap();
} }
#[derive(Validate, Serialize, Deserialize)] #[derive(Validate, Serialize, Deserialize)]
@@ -28,31 +27,11 @@ pub async fn req(session: Session, user: Option<User>, data: Json<Data>) -> Resu
data.validate() data.validate()
.map_err(|error| Error::FailedValidation { error })?; .map_err(|error| Error::FailedValidation { error })?;
if data.username == "revolt" { if User::is_username_taken(&data.username).await? {
Err(Error::UsernameTaken)? return Err(Error::UsernameTaken)
} }
let col = get_collection("users"); get_collection("users").insert_one(
if col
.find_one(
doc! {
"username": &data.username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.is_some()
{
Err(Error::UsernameTaken)?
}
col.insert_one(
doc! { doc! {
"_id": session.user_id, "_id": session.user_id,
"username": &data.username "username": &data.username
+4 -3
View File
@@ -16,7 +16,7 @@ pub struct Subscription {
#[post("/subscribe", data = "<data>")] #[post("/subscribe", data = "<data>")]
pub async fn req(session: Session, data: Json<Subscription>) -> Result<()> { pub async fn req(session: Session, data: Json<Subscription>) -> Result<()> {
let data = data.into_inner(); let data = data.into_inner();
let col = get_collection("accounts") get_collection("accounts")
.update_one( .update_one(
doc! { doc! {
"_id": session.user_id, "_id": session.user_id,
@@ -24,13 +24,14 @@ pub async fn req(session: Session, data: Json<Subscription>) -> Result<()> {
}, },
doc! { doc! {
"$set": { "$set": {
"sessions.$.subscription": to_document(&data).unwrap() "sessions.$.subscription": to_document(&data)
.map_err(|_| Error::DatabaseError { operation: "to_document", with: "subscription" })?
} }
}, },
None, None,
) )
.await .await
.unwrap(); .map_err(|_| Error::DatabaseError { operation: "update_one", with: "account" })?;
Ok(()) Ok(())
} }
+5 -2
View File
@@ -6,7 +6,7 @@ use rauth::auth::Session;
#[post("/unsubscribe")] #[post("/unsubscribe")]
pub async fn req(session: Session) -> Result<()> { pub async fn req(session: Session) -> Result<()> {
let col = get_collection("accounts") get_collection("accounts")
.update_one( .update_one(
doc! { doc! {
"_id": session.user_id, "_id": session.user_id,
@@ -20,7 +20,10 @@ pub async fn req(session: Session) -> Result<()> {
None, None,
) )
.await .await
.unwrap(); .map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "subscription",
})?;
Ok(()) Ok(())
} }
+1 -1
View File
@@ -9,7 +9,7 @@ use rocket_contrib::json::JsonValue;
#[get("/")] #[get("/")]
pub async fn root() -> JsonValue { pub async fn root() -> JsonValue {
json!({ json!({
"revolt": "0.3.3-alpha.6", "revolt": "0.3.3-alpha.7",
"features": { "features": {
"registration": !*DISABLE_REGISTRATION, "registration": !*DISABLE_REGISTRATION,
"captcha": { "captcha": {
+1 -1
View File
@@ -13,7 +13,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.await?; .await?;
if !perm.get_access() { if !perm.get_access() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
Ok(json!(target.from(&user).with(perm))) Ok(json!(target.from(&user).with(perm)))
+5 -8
View File
@@ -8,15 +8,12 @@ use rocket_contrib::json::JsonValue;
#[get("/<target>/mutual")] #[get("/<target>/mutual")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> { pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let channels = get_collection("channels") let users = get_collection("users")
.find( .find(
doc! { doc! {
"$or": [
{ "type": "Group" },
],
"$and": [ "$and": [
{ "recipients": &user.id }, { "relations._id": &user.id, "relations.status": "Friend" },
{ "recipients": &target.id } { "relations._id": &target.id, "relations.status": "Friend" }
] ]
}, },
FindOptions::builder().projection(doc! { "_id": 1 }).build(), FindOptions::builder().projection(doc! { "_id": 1 }).build(),
@@ -24,7 +21,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.await .await
.map_err(|_| Error::DatabaseError { .map_err(|_| Error::DatabaseError {
operation: "find", operation: "find",
with: "channels", with: "users",
})? })?
.filter_map(async move |s| s.ok()) .filter_map(async move |s| s.ok())
.collect::<Vec<Document>>() .collect::<Vec<Document>>()
@@ -33,5 +30,5 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string())) .filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>(); .collect::<Vec<String>>();
Ok(json!({ "channels": channels })) Ok(json!({ "users": users }))
} }
+9
View File
@@ -40,6 +40,8 @@ pub enum Error {
UnknownAttachment, UnknownAttachment,
#[snafu(display("Cannot edit someone else's message."))] #[snafu(display("Cannot edit someone else's message."))]
CannotEditMessage, CannotEditMessage,
#[snafu(display("Cannot send empty message."))]
EmptyMessage,
#[snafu(display("Cannot remove yourself from a group, use delete channel instead."))] #[snafu(display("Cannot remove yourself from a group, use delete channel instead."))]
CannotRemoveYourself, CannotRemoveYourself,
#[snafu(display("Group size is too large."))] #[snafu(display("Group size is too large."))]
@@ -50,6 +52,8 @@ pub enum Error {
NotInGroup, NotInGroup,
// ? General errors. // ? General errors.
#[snafu(display("Trying to fetch too much data."))]
TooManyIds,
#[snafu(display("Failed to validate fields."))] #[snafu(display("Failed to validate fields."))]
FailedValidation { error: ValidationErrors }, FailedValidation { error: ValidationErrors },
#[snafu(display("Encountered a database error."))] #[snafu(display("Encountered a database error."))]
@@ -59,6 +63,8 @@ pub enum Error {
}, },
#[snafu(display("Internal server error."))] #[snafu(display("Internal server error."))]
InternalError, InternalError,
#[snafu(display("Missing permission."))]
MissingPermission,
#[snafu(display("Operation cannot be performed on this object."))] #[snafu(display("Operation cannot be performed on this object."))]
InvalidOperation, InvalidOperation,
#[snafu(display("Already created an object with this nonce."))] #[snafu(display("Already created an object with this nonce."))]
@@ -88,6 +94,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::UnknownChannel => Status::NotFound, Error::UnknownChannel => Status::NotFound,
Error::UnknownAttachment => Status::BadRequest, Error::UnknownAttachment => Status::BadRequest,
Error::CannotEditMessage => Status::Forbidden, Error::CannotEditMessage => Status::Forbidden,
Error::EmptyMessage => Status::UnprocessableEntity,
Error::CannotRemoveYourself => Status::BadRequest, Error::CannotRemoveYourself => Status::BadRequest,
Error::GroupTooLarge { .. } => Status::Forbidden, Error::GroupTooLarge { .. } => Status::Forbidden,
Error::AlreadyInGroup => Status::Conflict, Error::AlreadyInGroup => Status::Conflict,
@@ -96,7 +103,9 @@ impl<'r> Responder<'r, 'static> for Error {
Error::FailedValidation { .. } => Status::UnprocessableEntity, Error::FailedValidation { .. } => Status::UnprocessableEntity,
Error::DatabaseError { .. } => Status::InternalServerError, Error::DatabaseError { .. } => Status::InternalServerError,
Error::InternalError => Status::InternalServerError, Error::InternalError => Status::InternalServerError,
Error::MissingPermission => Status::Forbidden,
Error::InvalidOperation => Status::BadRequest, Error::InvalidOperation => Status::BadRequest,
Error::TooManyIds => Status::BadRequest,
Error::DuplicateNonce => Status::Conflict, Error::DuplicateNonce => Status::Conflict,
Error::NoEffect => Status::Ok, Error::NoEffect => Status::Ok,
}; };
-2
View File
@@ -52,8 +52,6 @@ lazy_static! {
// Application Logic Settings // Application Logic Settings
pub static ref MAX_GROUP_SIZE: usize = pub static ref MAX_GROUP_SIZE: usize =
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 PUSH_LIMIT: usize =
env::var("REVOLT_PUSH_LIMIT").unwrap_or_else(|_| "50".to_string()).parse().unwrap();
} }
pub fn preflight_checks() { pub fn preflight_checks() {