Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 757 additions and 300 deletions
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use chrono::prelude::*;
use mongodb::bson::{doc, to_bson};
use mongodb::options::UpdateOptions;
use rocket::request::Form;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
type Data = HashMap<String, String>;
#[derive(Serialize, Deserialize, FromForm)]
pub struct Options {
timestamp: Option<i64>,
}
#[post("/settings/set?<options..>", data = "<data>")]
pub async fn req(user: User, data: Json<Data>, options: Form<Options>) -> Result<()> {
let data = data.into_inner();
let current_time = Utc::now().timestamp_millis();
let timestamp = if let Some(timestamp) = options.timestamp {
if timestamp > current_time {
current_time
} else {
timestamp
}
} else {
current_time
};
let mut set = doc! {};
for (key, data) in &data {
set.insert(
key.clone(),
vec![
to_bson(&timestamp).unwrap(),
to_bson(&data.clone()).unwrap(),
],
);
}
if set.len() > 0 {
get_collection("user_settings")
.update_one(
doc! {
"_id": &user.id
},
doc! {
"$set": &set
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user_settings",
})?;
}
ClientboundNotification::UserSettingsUpdate {
id: user.id.clone(),
update: json!(set),
}
.publish(user.id);
Ok(())
}
use crate::util::result::Result; use crate::database::*;
use crate::{ use crate::notifications::events::ClientboundNotification;
database::{ use crate::util::result::{Error, Result};
entities::{RelationshipStatus, User},
get_collection,
guards::reference::Ref,
permissions::get_relationship,
},
util::result::Error,
};
use futures::try_join; use futures::try_join;
use mongodb::bson::doc; use mongodb::bson::doc;
use mongodb::options::{Collation, FindOneOptions};
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
#[put("/<target>/friend")] #[put("/<username>/friend")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> { pub async fn req(user: User, username: String) -> Result<JsonValue> {
let col = get_collection("users"); let col = get_collection("users");
let doc = col
.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",
})?
.ok_or_else(|| Error::UnknownUser)?;
let target_id = doc.get_str("_id").map_err(|_| Error::DatabaseError {
operation: "get_str(_id)",
with: "user",
})?;
let target_user = Ref::from(target_id.to_string())?.fetch_user().await?;
match get_relationship(&user, &target) { match get_relationship(&user, &target_id) {
RelationshipStatus::User => return Err(Error::NoEffect), RelationshipStatus::User => return Err(Error::NoEffect),
RelationshipStatus::Friend => return Err(Error::AlreadyFriends), RelationshipStatus::Friend => return Err(Error::AlreadyFriends),
RelationshipStatus::Outgoing => return Err(Error::AlreadySentRequest), RelationshipStatus::Outgoing => return Err(Error::AlreadySentRequest),
...@@ -27,7 +44,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -27,7 +44,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
col.update_one( col.update_one(
doc! { doc! {
"_id": &user.id, "_id": &user.id,
"relations._id": &target.id "relations._id": target_id
}, },
doc! { doc! {
"$set": { "$set": {
...@@ -38,7 +55,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -38,7 +55,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
), ),
col.update_one( col.update_one(
doc! { doc! {
"_id": &target.id, "_id": target_id,
"relations._id": &user.id "relations._id": &user.id
}, },
doc! { doc! {
...@@ -49,7 +66,30 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -49,7 +66,30 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
None None
) )
) { ) {
Ok(_) => Ok(json!({ "status": "Friend" })), Ok(_) => {
let target_user = target_user
.from_override(&user, RelationshipStatus::Friend)
.await?;
let user = user
.from_override(&target_user, RelationshipStatus::Friend)
.await?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target_user,
status: RelationshipStatus::Friend,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.to_string(),
user,
status: RelationshipStatus::Friend,
}
.publish(target_id.to_string());
Ok(json!({ "status": "Friend" }))
}
Err(_) => Err(Error::DatabaseError { Err(_) => Err(Error::DatabaseError {
operation: "update_one", operation: "update_one",
with: "user", with: "user",
...@@ -65,7 +105,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -65,7 +105,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
doc! { doc! {
"$push": { "$push": {
"relations": { "relations": {
"_id": &target.id, "_id": target_id,
"status": "Outgoing" "status": "Outgoing"
} }
} }
...@@ -74,7 +114,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -74,7 +114,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
), ),
col.update_one( col.update_one(
doc! { doc! {
"_id": &target.id "_id": target_id
}, },
doc! { doc! {
"$push": { "$push": {
...@@ -87,7 +127,30 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -87,7 +127,30 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
None None
) )
) { ) {
Ok(_) => Ok(json!({ "status": "Outgoing" })), Ok(_) => {
let target_user = target_user
.from_override(&user, RelationshipStatus::Outgoing)
.await?;
let user = user
.from_override(&target_user, RelationshipStatus::Incoming)
.await?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target_user,
status: RelationshipStatus::Outgoing,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.to_string(),
user,
status: RelationshipStatus::Incoming,
}
.publish(target_id.to_string());
Ok(json!({ "status": "Outgoing" }))
}
Err(_) => Err(Error::DatabaseError { Err(_) => Err(Error::DatabaseError {
operation: "update_one", operation: "update_one",
with: "user", with: "user",
......
use crate::util::result::Result; use crate::database::*;
use crate::{ use crate::notifications::events::ClientboundNotification;
database::entities::RelationshipStatus, database::entities::User, database::get_collection, use crate::util::result::{Error, Result};
database::guards::reference::Ref, database::permissions::get_relationship, util::result::Error,
};
use futures::try_join; use futures::try_join;
use mongodb::bson::doc; use mongodb::bson::doc;
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
...@@ -11,7 +10,9 @@ use rocket_contrib::json::JsonValue; ...@@ -11,7 +10,9 @@ use rocket_contrib::json::JsonValue;
pub async fn req(user: User, target: Ref) -> Result<JsonValue> { pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let col = get_collection("users"); let col = get_collection("users");
match get_relationship(&user, &target) { let target = target.fetch_user().await?;
match get_relationship(&user, &target.id) {
RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect), RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect),
RelationshipStatus::BlockedOther => { RelationshipStatus::BlockedOther => {
col.update_one( col.update_one(
...@@ -32,6 +33,13 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -32,6 +33,13 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
with: "user", with: "user",
})?; })?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone());
Ok(json!({ "status": "Blocked" })) Ok(json!({ "status": "Blocked" }))
} }
RelationshipStatus::None => { RelationshipStatus::None => {
...@@ -65,7 +73,31 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -65,7 +73,31 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
None None
) )
) { ) {
Ok(_) => Ok(json!({ "status": "Blocked" })), Ok(_) => {
let target = target
.from_override(&user, RelationshipStatus::Blocked)
.await?;
let user = user
.from_override(&target, RelationshipStatus::BlockedOther)
.await?;
let target_id = target.id.clone();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user,
status: RelationshipStatus::BlockedOther,
}
.publish(target_id);
Ok(json!({ "status": "Blocked" }))
}
Err(_) => Err(Error::DatabaseError { Err(_) => Err(Error::DatabaseError {
operation: "update_one", operation: "update_one",
with: "user", with: "user",
...@@ -101,7 +133,31 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -101,7 +133,31 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
None None
) )
) { ) {
Ok(_) => Ok(json!({ "status": "Blocked" })), Ok(_) => {
let target = target
.from_override(&user, RelationshipStatus::Blocked)
.await?;
let user = user
.from_override(&target, RelationshipStatus::BlockedOther)
.await?;
let target_id = target.id.clone();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user,
status: RelationshipStatus::BlockedOther,
}
.publish(target_id);
Ok(json!({ "status": "Blocked" }))
}
Err(_) => Err(Error::DatabaseError { Err(_) => Err(Error::DatabaseError {
operation: "update_one", operation: "update_one",
with: "user", with: "user",
......
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rauth::auth::{Auth, Session};
use regex::Regex;
use rocket::State;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
// ! FIXME: should be global somewhere; maybe use config(?)
lazy_static! {
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap();
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
username: Option<String>,
#[validate(length(min = 8, max = 72))]
password: String,
}
#[patch("/<_ignore_id>/username", data = "<data>")]
pub async fn req(
auth: State<'_, Auth>,
session: Session,
user: User,
data: Json<Data>,
_ignore_id: String,
) -> Result<()> {
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
auth.verify_password(&session, data.password.clone())
.await
.map_err(|_| Error::InvalidCredentials)?;
let mut set = doc! {};
if let Some(username) = &data.username {
if User::is_username_taken(&username).await? {
return Err(Error::UsernameTaken);
}
set.insert("username", username.clone());
}
get_collection("users")
.update_one(doc! { "_id": &user.id }, doc! { "$set": set }, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!({
"username": data.username
}),
clear: None,
}
.publish_as_user(user.id.clone());
Ok(())
}
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use crate::{database::*, notifications::events::RemoveUserField};
use mongodb::bson::{doc, to_document};
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize, Debug)]
pub struct UserProfileData {
#[validate(length(min = 0, max = 2000))]
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128))]
background: Option<String>,
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate]
status: Option<UserStatus>,
#[validate]
profile: Option<UserProfileData>,
#[validate(length(min = 1, max = 128))]
avatar: Option<String>,
remove: Option<RemoveUserField>,
}
#[patch("/<_ignore_id>", data = "<data>")]
pub async fn req(user: User, data: Json<Data>, _ignore_id: String) -> Result<()> {
let mut data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.status.is_none()
&& data.profile.is_none()
&& data.avatar.is_none()
&& data.remove.is_none()
{
return Ok(());
}
let mut unset = doc! {};
let mut set = doc! {};
let mut remove_background = false;
let mut remove_avatar = false;
if let Some(remove) = &data.remove {
match remove {
RemoveUserField::ProfileContent => {
unset.insert("profile.content", 1);
}
RemoveUserField::ProfileBackground => {
unset.insert("profile.background", 1);
remove_background = true;
}
RemoveUserField::StatusText => {
unset.insert("status.text", 1);
}
RemoveUserField::Avatar => {
unset.insert("avatar", 1);
remove_avatar = true;
}
}
}
if let Some(status) = &data.status {
set.insert(
"status",
to_document(&status).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "status",
})?,
);
}
if let Some(profile) = data.profile {
if let Some(content) = profile.content {
set.insert("profile.content", content);
}
if let Some(attachment_id) = profile.background {
let attachment =
File::find_and_use(&attachment_id, "backgrounds", "user", &user.id).await?;
set.insert(
"profile.background",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_background = true;
}
}
let avatar = std::mem::replace(&mut data.avatar, None);
if let Some(attachment_id) = avatar {
let attachment = File::find_and_use(&attachment_id, "avatars", "user", &user.id).await?;
set.insert(
"avatar",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_avatar = true;
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("users")
.update_one(doc! { "_id": &user.id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
}
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!(set),
clear: data.remove,
}
.publish_as_user(user.id.clone());
if remove_avatar {
if let Some(old_avatar) = user.avatar {
old_avatar.delete().await?;
}
}
if remove_background {
if let Some(profile) = user.profile {
if let Some(old_background) = profile.background {
old_background.delete().await?;
}
}
}
Ok(())
}
use crate::database::entities::{Channel, User}; use crate::database::*;
use crate::database::get_collection;
use crate::util::result::{Error, Result}; use crate::util::result::{Error, Result};
use futures::StreamExt; use futures::StreamExt;
use mongodb::bson::{doc, from_bson, Bson}; use mongodb::bson::doc;
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
#[get("/dms")] #[get("/dms")]
...@@ -12,11 +12,11 @@ pub async fn req(user: User) -> Result<JsonValue> { ...@@ -12,11 +12,11 @@ pub async fn req(user: User) -> Result<JsonValue> {
doc! { doc! {
"$or": [ "$or": [
{ {
"type": "DirectMessage", "channel_type": "DirectMessage",
"active": true "active": true
}, },
{ {
"type": "Group" "channel_type": "Group"
} }
], ],
"recipients": user.id "recipients": user.id
......
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
#[get("/<target>/profile")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let target = target.fetch_user().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_user(&target)
.for_user_given()
.await?;
if !perm.get_view_profile() {
Err(Error::MissingPermission)?
}
if target.profile.is_some() {
Ok(json!(target.profile))
} else {
Ok(json!({}))
}
}
use crate::database::{entities::User, guards::reference::Ref, permissions::get_relationship}; use crate::database::*;
use crate::util::result::Result; use crate::util::result::Result;
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
#[get("/<target>/relationship")] #[get("/<target>/relationship")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> { pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
Ok(json!({ "status": get_relationship(&user, &target) })) Ok(json!({ "status": get_relationship(&user, &target.id) }))
} }
use crate::database::entities::User; use crate::database::*;
use crate::util::result::Result; use crate::util::result::Result;
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
#[get("/relationships")] #[get("/relationships")]
......
use crate::database::entities::User; use crate::database::*;
use crate::database::guards::reference::Ref;
use crate::util::result::{Error, Result}; use crate::util::result::{Error, Result};
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
#[get("/<target>")] #[get("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> { pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let mut target = target.fetch_user().await?; let target = target.fetch_user().await?;
if user.id != target.id { let perm = permissions::PermissionCalculator::new(&user)
// Check whether we are allowed to fetch this user. .with_user(&target)
let perm = crate::database::permissions::temp_calc_perm(&user, &target).await; .for_user_given()
if !perm.get_access() { .await?;
Err(Error::LabelMe)?
}
// Only return user relationships if the target is the caller. if !perm.get_access() {
target.relations = None; Err(Error::MissingPermission)?
} }
Ok(json!(target)) Ok(json!(target.from(&user).with(perm)))
} }
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, Document};
use mongodb::options::FindOptions;
use rocket_contrib::json::JsonValue;
#[get("/<target>/mutual")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let users = get_collection("users")
.find(
doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user.id, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &target.id, "status": "Friend" } } }
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>();
Ok(json!({ "users": users }))
}
use rocket::{Request, Response};
use rocket::response::{self, NamedFile, Responder};
use std::path::Path;
use crate::database::Ref;
pub struct CachedFile(NamedFile);
pub static CACHE_CONTROL: &'static str = "public, max-age=31536000, immutable";
impl<'r> Responder<'r, 'static> for CachedFile {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
Response::build_from(self.0.respond_to(req)?)
.raw_header("Cache-control", CACHE_CONTROL)
.ok()
}
}
#[get("/<target>/default_avatar")]
pub async fn req(target: Ref) -> Option<CachedFile> {
match target.id.chars().nth(25).unwrap() {
'0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' => {
NamedFile::open(Path::new("assets/user_red.png")).await.ok().map(|n| CachedFile(n))
}
'8' | '9' | 'A' | 'C' | 'B' | 'D' | 'E' | 'F' => {
NamedFile::open(Path::new("assets/user_green.png"))
.await
.ok().map(|n| CachedFile(n))
}
'G' | 'H' | 'J' | 'K' | 'M' | 'N' | 'P' | 'Q' => {
NamedFile::open(Path::new("assets/user_blue.png"))
.await
.ok().map(|n| CachedFile(n))
}
'R' | 'S' | 'T' | 'V' | 'W' | 'X' | 'Y' | 'Z' => {
NamedFile::open(Path::new("assets/user_yellow.png"))
.await
.ok().map(|n| CachedFile(n))
}
_ => unreachable!(),
}
}
...@@ -2,10 +2,15 @@ use rocket::Route; ...@@ -2,10 +2,15 @@ use rocket::Route;
mod add_friend; mod add_friend;
mod block_user; mod block_user;
mod change_username;
mod edit_user;
mod fetch_dms; mod fetch_dms;
mod fetch_profile;
mod fetch_relationship; mod fetch_relationship;
mod fetch_relationships; mod fetch_relationships;
mod fetch_user; mod fetch_user;
mod find_mutual;
mod get_default_avatar;
mod open_dm; mod open_dm;
mod remove_friend; mod remove_friend;
mod unblock_user; mod unblock_user;
...@@ -14,10 +19,15 @@ pub fn routes() -> Vec<Route> { ...@@ -14,10 +19,15 @@ pub fn routes() -> Vec<Route> {
routes![ routes![
// User Information // User Information
fetch_user::req, fetch_user::req,
edit_user::req,
change_username::req,
get_default_avatar::req,
fetch_profile::req,
// Direct Messaging // Direct Messaging
fetch_dms::req, fetch_dms::req,
open_dm::req, open_dm::req,
// Relationships // Relationships
find_mutual::req,
fetch_relationships::req, fetch_relationships::req,
fetch_relationship::req, fetch_relationship::req,
add_friend::req, add_friend::req,
......
use crate::database::entities::{Channel, User}; use crate::database::*;
use crate::database::get_collection;
use crate::database::guards::reference::Ref;
use crate::util::result::{Error, Result}; use crate::util::result::{Error, Result};
use mongodb::bson::doc; use mongodb::bson::doc;
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
use ulid::Ulid; use ulid::Ulid;
...@@ -10,12 +9,12 @@ use ulid::Ulid; ...@@ -10,12 +9,12 @@ use ulid::Ulid;
pub async fn req(user: User, target: Ref) -> Result<JsonValue> { pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let query = if user.id == target.id { let query = if user.id == target.id {
doc! { doc! {
"type": "SavedMessages", "channel_type": "SavedMessages",
"user": &user.id "user": &user.id
} }
} else { } else {
doc! { doc! {
"type": "DirectMessage", "channel_type": "DirectMessage",
"recipients": { "recipients": {
"$all": [ &user.id, &target.id ] "$all": [ &user.id, &target.id ]
} }
...@@ -41,10 +40,11 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -41,10 +40,11 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
id, id,
active: false, active: false,
recipients: vec![user.id, target.id], recipients: vec![user.id, target.id],
last_message: None,
} }
}; };
channel.save().await?; channel.clone().publish().await?;
Ok(json!(channel)) Ok(json!(channel))
} }
} }
use crate::util::result::Result; use crate::database::*;
use crate::{ use crate::notifications::events::ClientboundNotification;
database::entities::RelationshipStatus, database::entities::User, database::get_collection, use crate::util::result::{Error, Result};
database::guards::reference::Ref, database::permissions::get_relationship, util::result::Error,
};
use futures::try_join; use futures::try_join;
use mongodb::bson::doc; use mongodb::bson::doc;
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
...@@ -11,11 +10,9 @@ use rocket_contrib::json::JsonValue; ...@@ -11,11 +10,9 @@ use rocket_contrib::json::JsonValue;
pub async fn req(user: User, target: Ref) -> Result<JsonValue> { pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let col = get_collection("users"); let col = get_collection("users");
match get_relationship(&user, &target) { let target = target.fetch_user().await?;
RelationshipStatus::Blocked
| RelationshipStatus::BlockedOther match get_relationship(&user, &target.id) {
| RelationshipStatus::User
| RelationshipStatus::None => Err(Error::NoEffect),
RelationshipStatus::Friend RelationshipStatus::Friend
| RelationshipStatus::Outgoing | RelationshipStatus::Outgoing
| RelationshipStatus::Incoming => { | RelationshipStatus::Incoming => {
...@@ -47,12 +44,37 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> { ...@@ -47,12 +44,37 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
None None
) )
) { ) {
Ok(_) => Ok(json!({ "status": "None" })), Ok(_) => {
let target = target
.from_override(&user, RelationshipStatus::None)
.await?;
let user = user
.from_override(&target, RelationshipStatus::None)
.await?;
let target_id = target.id.clone();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::None,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user,
status: RelationshipStatus::None,
}
.publish(target_id);
Ok(json!({ "status": "None" }))
}
Err(_) => Err(Error::DatabaseError { Err(_) => Err(Error::DatabaseError {
operation: "update_one", operation: "update_one",
with: "user", with: "user",
}), }),
} }
} }
_ => Err(Error::NoEffect),
} }
} }
use crate::util::result::Result; use crate::database::*;
use crate::{ use crate::notifications::events::ClientboundNotification;
database::entities::RelationshipStatus, database::entities::User, database::get_collection, use crate::util::result::{Error, Result};
database::guards::reference::Ref, database::permissions::get_relationship, util::result::Error,
};
use futures::try_join; use futures::try_join;
use mongodb::bson::doc; use mongodb::bson::doc;
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
...@@ -10,75 +9,103 @@ use rocket_contrib::json::JsonValue; ...@@ -10,75 +9,103 @@ use rocket_contrib::json::JsonValue;
#[delete("/<target>/block")] #[delete("/<target>/block")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> { pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let col = get_collection("users"); let col = get_collection("users");
let target = target.fetch_user().await?;
match get_relationship(&user, &target.id) {
RelationshipStatus::Blocked => match get_relationship(&target, &user.id) {
RelationshipStatus::Blocked => {
col.update_one(
doc! {
"_id": &user.id,
"relations._id": &target.id
},
doc! {
"$set": {
"relations.$.status": "BlockedOther"
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
let target = target
.from_override(&user, RelationshipStatus::BlockedOther)
.await?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::BlockedOther,
}
.publish(user.id.clone());
match get_relationship(&user, &target) { Ok(json!({ "status": "BlockedOther" }))
RelationshipStatus::None }
| RelationshipStatus::User RelationshipStatus::BlockedOther => {
| RelationshipStatus::BlockedOther match try_join!(
| RelationshipStatus::Incoming col.update_one(
| RelationshipStatus::Outgoing doc! {
| RelationshipStatus::Friend => Err(Error::NoEffect), "_id": &user.id
RelationshipStatus::Blocked => { },
match get_relationship(&target.fetch_user().await?, &user.as_ref()) { doc! {
RelationshipStatus::Blocked => { "$pull": {
"relations": {
"_id": &target.id
}
}
},
None
),
col.update_one( col.update_one(
doc! { doc! {
"_id": &user.id, "_id": &target.id
"relations._id": &target.id
}, },
doc! { doc! {
"$set": { "$pull": {
"relations.$.status": "BlockedOther" "relations": {
"_id": &user.id
}
} }
}, },
None, None
) )
.await ) {
.map_err(|_| Error::DatabaseError { Ok(_) => {
operation: "update_one", let target = target
with: "user", .from_override(&user, RelationshipStatus::None)
})?; .await?;
let user = user
.from_override(&target, RelationshipStatus::None)
.await?;
let target_id = target.id.clone();
Ok(json!({ "status": "BlockedOther" })) ClientboundNotification::UserRelationship {
} id: user.id.clone(),
RelationshipStatus::BlockedOther => { user: target,
match try_join!( status: RelationshipStatus::None,
col.update_one( }
doc! { .publish(user.id.clone());
"_id": &user.id
}, ClientboundNotification::UserRelationship {
doc! { id: target_id.clone(),
"$pull": { user: user,
"relations": { status: RelationshipStatus::None,
"_id": &target.id }
} .publish(target_id);
}
}, Ok(json!({ "status": "None" }))
None
),
col.update_one(
doc! {
"_id": &target.id
},
doc! {
"$pull": {
"relations": {
"_id": &user.id
}
}
},
None
)
) {
Ok(_) => Ok(json!({ "status": "None" })),
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
}),
} }
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
}),
} }
_ => Err(Error::InternalError),
} }
} _ => Err(Error::InternalError),
},
_ => Err(Error::NoEffect),
} }
} }
use crate::util::variables::{HCAPTCHA_KEY, USE_HCAPTCHA};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
struct CaptchaResponse {
success: bool,
}
pub async fn verify(user_token: &Option<String>) -> Result<(), String> {
if *USE_HCAPTCHA {
if let Some(token) = user_token {
let mut map = HashMap::new();
map.insert("secret", HCAPTCHA_KEY.to_string());
map.insert("response", token.to_string());
let client = Client::new();
if let Ok(response) = client
.post("https://hcaptcha.com/siteverify")
.form(&map)
.send()
.await
{
let result: CaptchaResponse = response
.json()
.await
.map_err(|_| "Failed to deserialise captcha result.".to_string())?;
if result.success {
Ok(())
} else {
Err("Unsuccessful captcha verification".to_string())
}
} else {
Err("Failed to verify with hCaptcha".to_string())
}
} else {
Err("Missing hCaptcha token!".to_string())
}
} else {
Ok(())
}
}
use lettre::message::{header, MultiPart, SinglePart};
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
use super::variables::{PUBLIC_URL, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_USERNAME};
lazy_static! {
static ref MAILER: lettre::transport::smtp::SmtpTransport =
SmtpTransport::relay(SMTP_HOST.as_ref())
.unwrap()
.credentials(Credentials::new(
SMTP_USERNAME.to_string(),
SMTP_PASSWORD.to_string()
))
.build();
}
fn send(message: Message) -> Result<(), String> {
MAILER
.send(&message)
.map_err(|err| format!("Failed to send email! {}", err.to_string()))?;
Ok(())
}
fn generate_multipart(text: &str, html: &str) -> MultiPart {
MultiPart::mixed().multipart(
MultiPart::alternative()
.singlepart(
SinglePart::quoted_printable()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.body(text),
)
.multipart(
MultiPart::related().singlepart(
SinglePart::eight_bit()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.body(html),
),
),
)
}
pub fn send_verification_email(email: String, code: String) -> Result<(), String> {
let url = format!("{}/api/account/verify/{}", PUBLIC_URL.to_string(), code);
let email = Message::builder()
.from(SMTP_FROM.to_string().parse().unwrap())
.to(email.parse().unwrap())
.subject("Verify your email!")
.multipart(generate_multipart(
&format!("Verify your email here: {}", url),
&format!("<a href=\"{}\">Click to verify your email!</a>", url),
))
.unwrap();
send(email)
}
pub fn send_password_reset(email: String, code: String) -> Result<(), String> {
let url = format!("{}/api/account/reset/{}", PUBLIC_URL.to_string(), code);
let email = Message::builder()
.from(SMTP_FROM.to_string().parse().unwrap())
.to(email.parse().unwrap())
.subject("Reset your password.")
.multipart(generate_multipart(
&format!("Reset your password here: {}", url),
&format!("<a href=\"{}\">Click to reset your password!</a>", url),
))
.unwrap();
send(email)
}
pub fn send_welcome_email(email: String, username: String) -> Result<(), String> {
let email = Message::builder()
.from(SMTP_FROM.to_string().parse().unwrap())
.to(email.parse().unwrap())
.subject("Welcome to REVOLT!")
.multipart(
generate_multipart(
&format!("Welcome, {}! You can now use REVOLT.", username),
&format!(
"<b>Welcome, {}!</b><br/>You can now use REVOLT.<br/><a href=\"{}\">Go to REVOLT</a>",
username,
PUBLIC_URL.to_string()
)
)
)
.unwrap();
send(email)
}
use rand::{distributions::Alphanumeric, Rng};
use std::collections::HashSet;
use std::iter::FromIterator;
pub mod captcha;
pub mod email;
pub mod result; pub mod result;
pub mod variables; pub mod variables;
pub fn vec_to_set<T: Clone + Eq + std::hash::Hash>(data: &[T]) -> HashSet<T> {
HashSet::from_iter(data.iter().cloned())
}
pub fn gen_token(l: usize) -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(l)
.collect::<String>()
}
...@@ -3,45 +3,63 @@ use rocket::http::{ContentType, Status}; ...@@ -3,45 +3,63 @@ use rocket::http::{ContentType, Status};
use rocket::request::Request; use rocket::request::Request;
use rocket::response::{self, Responder, Response}; use rocket::response::{self, Responder, Response};
use serde::Serialize; use serde::Serialize;
use snafu::Snafu;
use std::io::Cursor; use std::io::Cursor;
use validator::ValidationErrors; use validator::ValidationErrors;
#[derive(Serialize, Debug, Snafu)] #[derive(Serialize, Debug)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum Error { pub enum Error {
#[snafu(display("This error has not been labelled."))]
LabelMe, LabelMe,
// ? Onboarding related errors. // ? Onboarding related errors.
#[snafu(display("Already finished onboarding."))]
AlreadyOnboarded, AlreadyOnboarded,
// ? User related errors. // ? User related errors.
#[snafu(display("Username has already been taken."))]
UsernameTaken, UsernameTaken,
#[snafu(display("This user does not exist!"))]
UnknownUser, UnknownUser,
#[snafu(display("Already friends with this user."))]
AlreadyFriends, AlreadyFriends,
#[snafu(display("Already sent a request to this user."))]
AlreadySentRequest, AlreadySentRequest,
#[snafu(display("You have blocked this user."))]
Blocked, Blocked,
#[snafu(display("You have been blocked by this user."))]
BlockedByOther, BlockedByOther,
NotFriends,
// ? Channel related errors.
UnknownChannel,
UnknownAttachment,
UnknownMessage,
CannotEditMessage,
CannotJoinCall,
TooManyAttachments,
TooManyReplies,
EmptyMessage,
CannotRemoveYourself,
GroupTooLarge {
max: usize,
},
AlreadyInGroup,
NotInGroup,
// ? Server related errors.
UnknownServer,
InvalidRole,
Banned,
// ? General errors. // ? General errors.
#[snafu(display("Failed to validate fields."))] TooManyIds,
FailedValidation { error: ValidationErrors }, FailedValidation {
#[snafu(display("Encountered a database error."))] error: ValidationErrors,
},
DatabaseError { DatabaseError {
operation: &'static str, operation: &'static str,
with: &'static str, with: &'static str,
}, },
#[snafu(display("Internal server error."))]
InternalError, InternalError,
#[snafu(display("This request had no effect."))] MissingPermission,
InvalidOperation,
InvalidCredentials,
DuplicateNonce,
VosoUnavailable,
NotFound,
NoEffect, NoEffect,
} }
...@@ -61,10 +79,35 @@ impl<'r> Responder<'r, 'static> for Error { ...@@ -61,10 +79,35 @@ impl<'r> Responder<'r, 'static> for Error {
Error::AlreadySentRequest => Status::Conflict, Error::AlreadySentRequest => Status::Conflict,
Error::Blocked => Status::Conflict, Error::Blocked => Status::Conflict,
Error::BlockedByOther => Status::Forbidden, Error::BlockedByOther => Status::Forbidden,
Error::NotFriends => Status::Forbidden,
Error::UnknownChannel => Status::NotFound,
Error::UnknownMessage => Status::NotFound,
Error::UnknownAttachment => Status::BadRequest,
Error::CannotEditMessage => Status::Forbidden,
Error::CannotJoinCall => Status::BadRequest,
Error::TooManyAttachments => Status::BadRequest,
Error::TooManyReplies => Status::BadRequest,
Error::EmptyMessage => Status::UnprocessableEntity,
Error::CannotRemoveYourself => Status::BadRequest,
Error::GroupTooLarge { .. } => Status::Forbidden,
Error::AlreadyInGroup => Status::Conflict,
Error::NotInGroup => Status::NotFound,
Error::UnknownServer => Status::NotFound,
Error::InvalidRole => Status::NotFound,
Error::Banned => Status::Forbidden,
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::TooManyIds => Status::BadRequest,
Error::InvalidCredentials => Status::Forbidden,
Error::DuplicateNonce => Status::Conflict,
Error::VosoUnavailable => Status::BadRequest,
Error::NotFound => Status::NotFound,
Error::NoEffect => Status::Ok, Error::NoEffect => Status::Ok,
}; };
......