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 1093 additions and 899 deletions
use rocket::Route;
mod get_settings;
mod get_unreads;
mod set_settings;
pub fn routes() -> Vec<Route> {
routes![get_settings::req, set_settings::req, get_unreads::req]
}
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 super::Response;
use crate::database::{
self, get_relationship, get_relationship_internal, user::User, Relationship,
};
use crate::notifications::{
self,
events::{users::*, Notification},
};
use crate::routes::channel;
use mongodb::bson::doc;
use mongodb::options::{Collation, FindOneOptions, FindOptions};
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
/// retrieve your user information
#[get("/@me")]
pub fn me(user: User) -> Response {
Response::Success(user.serialise(Relationship::SELF as i32))
}
/// retrieve another user's information
#[get("/<target>")]
pub fn user(user: User, target: User) -> Response {
let relationship = get_relationship(&user, &target) as i32;
Response::Success(user.serialise(relationship))
}
#[derive(Serialize, Deserialize)]
pub struct UserQuery {
username: String,
}
/// find a user by their username
#[post("/query", data = "<query>")]
pub fn query(user: User, query: Json<UserQuery>) -> Response {
let col = database::get_collection("users");
if let Ok(result) = col.find_one(
doc! { "username": query.username.clone() },
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
) {
if let Some(doc) = result {
let id = doc.get_str("_id").unwrap();
Response::Success(json!({
"id": id,
"username": doc.get_str("username").unwrap(),
"display_name": doc.get_str("display_name").unwrap(),
"relationship": get_relationship_internal(&user.id, &id, &user.relations) as i32
}))
} else {
Response::NotFound(json!({
"error": "User not found!"
}))
}
} else {
Response::InternalServerError(json!({ "error": "Failed database query." }))
}
}
/*#[derive(Serialize, Deserialize)]
pub struct LookupQuery {
username: String,
}
/// lookup a user on Revolt
/// currently only supports exact username searches
#[post("/lookup", data = "<query>")]
pub fn lookup(user: User, query: Json<LookupQuery>) -> Response {
let relationships = user.fetch_relationships();
let col = database::get_collection("users");
if let Ok(users) = col.find(
doc! { "username": query.username.clone() },
FindOptions::builder()
.projection(doc! { "_id": 1, "username": 1 })
.limit(10)
.build(),
) {
let mut results = Vec::new();
for item in users {
if let Ok(doc) = item {
let id = doc.get_str("id").unwrap();
results.push(json!({
"id": id,
"username": doc.get_str("username").unwrap(),
"relationship": get_relationship_internal(&user.id, &id, &relationships) as i32
}));
}
}
Response::Success(json!(results))
} else {
Response::InternalServerError(json!({ "error": "Failed database query." }))
}
}*/
/// retrieve all of your DMs
#[get("/@me/dms")]
pub fn dms(user: User) -> Response {
let col = database::get_collection("channels");
if let Ok(results) = col.find(
doc! {
"$or": [
{
"type": channel::ChannelType::DM as i32,
"active": true
},
{
"type": channel::ChannelType::GROUPDM as i32
}
],
"recipients": user.id
},
FindOptions::builder().projection(doc! {}).build(),
) {
let mut channels = Vec::new();
for item in results {
if let Ok(doc) = item {
let id = doc.get_str("_id").unwrap();
let last_message = doc.get_document("last_message").unwrap();
let recipients = doc.get_array("recipients").unwrap();
match doc.get_i32("type").unwrap() {
0 => {
channels.push(json!({
"id": id,
"type": 0,
"last_message": last_message,
"recipients": recipients,
}));
}
1 => {
channels.push(json!({
"id": id,
"type": 1,
"recipients": recipients,
"name": doc.get_str("name").unwrap(),
"owner": doc.get_str("owner").unwrap(),
"description": doc.get_str("description").unwrap_or(""),
}));
}
_ => unreachable!(),
}
}
}
Response::Success(json!(channels))
} else {
Response::InternalServerError(json!({ "error": "Failed database query." }))
}
}
/// open a DM with a user
#[get("/<target>/dm")]
pub fn dm(user: User, target: User) -> Response {
let col = database::get_collection("channels");
if let Ok(result) = col.find_one(
doc! { "type": channel::ChannelType::DM as i32, "recipients": { "$all": [ user.id.clone(), target.id.clone() ] } },
None
) {
if let Some(channel) = result {
Response::Success( json!({ "id": channel.get_str("_id").unwrap() }))
} else {
let id = Ulid::new();
if col.insert_one(
doc! {
"_id": id.to_string(),
"type": channel::ChannelType::DM as i32,
"recipients": [ user.id, target.id ],
"active": false
},
None
).is_ok() {
Response::Success(json!({ "id": id.to_string() }))
} else {
Response::InternalServerError(json!({ "error": "Failed to create new channel." }))
}
}
} else {
Response::InternalServerError(json!({ "error": "Failed server query." }))
}
}
/// retrieve all of your friends
#[get("/@me/friend")]
pub fn get_friends(user: User) -> Response {
let mut results = Vec::new();
if let Some(arr) = user.relations {
for item in arr {
results.push(json!({
"id": item.id,
"status": item.status
}))
}
}
Response::Success(json!(results))
}
/// retrieve friend status with user
#[get("/<target>/friend")]
pub fn get_friend(user: User, target: User) -> Response {
Response::Success(json!({ "status": get_relationship(&user, &target) as i32 }))
}
/// create or accept a friend request
#[put("/<target>/friend")]
pub fn add_friend(user: User, target: User) -> Response {
let col = database::get_collection("users");
match get_relationship(&user, &target) {
Relationship::Friend => Response::BadRequest(json!({ "error": "Already friends." })),
Relationship::Outgoing => {
Response::BadRequest(json!({ "error": "Already sent a friend request." }))
}
Relationship::Incoming => {
if col
.update_one(
doc! {
"_id": user.id.clone(),
"relations.id": target.id.clone()
},
doc! {
"$set": {
"relations.$.status": Relationship::Friend as i32
}
},
None,
)
.is_ok()
{
if col
.update_one(
doc! {
"_id": target.id.clone(),
"relations.id": user.id.clone()
},
doc! {
"$set": {
"relations.$.status": Relationship::Friend as i32
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
vec![target.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: target.id.clone(),
user: user.id.clone(),
status: Relationship::Friend as i32,
}),
);
notifications::send_message_threaded(
vec![user.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: user.id.clone(),
user: target.id.clone(),
status: Relationship::Friend as i32,
}),
);
Response::Success(json!({ "status": Relationship::Friend as i32 }))
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit! Try re-adding them as a friend." }),
)
}
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit to database, try again." }),
)
}
}
Relationship::Blocked => {
Response::BadRequest(json!({ "error": "You have blocked this person." }))
}
Relationship::BlockedOther => {
Response::Conflict(json!({ "error": "You have been blocked by this person." }))
}
Relationship::NONE => {
if col
.update_one(
doc! {
"_id": user.id.clone()
},
doc! {
"$push": {
"relations": {
"id": target.id.clone(),
"status": Relationship::Outgoing as i32
}
}
},
None,
)
.is_ok()
{
if col
.update_one(
doc! {
"_id": target.id.clone()
},
doc! {
"$push": {
"relations": {
"id": user.id.clone(),
"status": Relationship::Incoming as i32
}
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
vec![user.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: user.id.clone(),
user: target.id.clone(),
status: Relationship::Outgoing as i32,
}),
);
notifications::send_message_threaded(
vec![target.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: target.id.clone(),
user: user.id.clone(),
status: Relationship::Incoming as i32,
}),
);
Response::Success(json!({ "status": Relationship::Outgoing as i32 }))
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit! Try re-adding them as a friend." }),
)
}
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit to database, try again." }),
)
}
}
Relationship::SELF => {
Response::BadRequest(json!({ "error": "You're already friends with yourself, no? c:" }))
}
}
}
/// remove a friend or deny a request
#[delete("/<target>/friend")]
pub fn remove_friend(user: User, target: User) -> Response {
let col = database::get_collection("users");
match get_relationship(&user, &target) {
Relationship::Friend | Relationship::Outgoing | Relationship::Incoming => {
if col
.update_one(
doc! {
"_id": user.id.clone()
},
doc! {
"$pull": {
"relations": {
"id": target.id.clone()
}
}
},
None,
)
.is_ok()
{
if col
.update_one(
doc! {
"_id": target.id.clone()
},
doc! {
"$pull": {
"relations": {
"id": user.id.clone()
}
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
vec![user.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: user.id.clone(),
user: target.id.clone(),
status: Relationship::NONE as i32,
}),
);
notifications::send_message_threaded(
vec![target.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: target.id.clone(),
user: user.id.clone(),
status: Relationship::NONE as i32,
}),
);
Response::Success(json!({ "status": Relationship::NONE as i32 }))
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit! Target remains in same state." }),
)
}
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit to database, try again." }),
)
}
}
Relationship::Blocked
| Relationship::BlockedOther
| Relationship::NONE
| Relationship::SELF => Response::BadRequest(json!({ "error": "This has no effect." })),
}
}
/// block a user
#[put("/<target>/block")]
pub fn block_user(user: User, target: User) -> Response {
let col = database::get_collection("users");
match get_relationship(&user, &target) {
Relationship::Friend | Relationship::Incoming | Relationship::Outgoing => {
if col
.update_one(
doc! {
"_id": user.id.clone(),
"relations.id": target.id.clone()
},
doc! {
"$set": {
"relations.$.status": Relationship::Blocked as i32
}
},
None,
)
.is_ok()
{
if col
.update_one(
doc! {
"_id": target.id.clone(),
"relations.id": user.id.clone()
},
doc! {
"$set": {
"relations.$.status": Relationship::BlockedOther as i32
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
vec![user.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: user.id.clone(),
user: target.id.clone(),
status: Relationship::Blocked as i32,
}),
);
notifications::send_message_threaded(
vec![target.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: target.id.clone(),
user: user.id.clone(),
status: Relationship::BlockedOther as i32,
}),
);
Response::Success(json!({ "status": Relationship::Blocked as i32 }))
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit! Try blocking the user again, remove it first." }),
)
}
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit to database, try again." }),
)
}
}
Relationship::NONE => {
if col
.update_one(
doc! {
"_id": user.id.clone(),
},
doc! {
"$push": {
"relations": {
"id": target.id.clone(),
"status": Relationship::Blocked as i32,
}
}
},
None,
)
.is_ok()
{
if col
.update_one(
doc! {
"_id": target.id.clone(),
},
doc! {
"$push": {
"relations": {
"id": user.id.clone(),
"status": Relationship::BlockedOther as i32,
}
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
vec![user.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: user.id.clone(),
user: target.id.clone(),
status: Relationship::Blocked as i32,
}),
);
notifications::send_message_threaded(
vec![target.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: target.id.clone(),
user: user.id.clone(),
status: Relationship::BlockedOther as i32,
}),
);
Response::Success(json!({ "status": Relationship::Blocked as i32 }))
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit! Try blocking the user again, remove it first." }),
)
}
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit to database, try again." }),
)
}
}
Relationship::Blocked => {
Response::BadRequest(json!({ "error": "Already blocked this person." }))
}
Relationship::BlockedOther => {
if col
.update_one(
doc! {
"_id": user.id.clone(),
"relations.id": target.id.clone()
},
doc! {
"$set": {
"relations.$.status": Relationship::Blocked as i32
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
vec![user.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: user.id.clone(),
user: target.id.clone(),
status: Relationship::Blocked as i32,
}),
);
Response::Success(json!({ "status": Relationship::Blocked as i32 }))
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit to database, try again." }),
)
}
}
Relationship::SELF => Response::BadRequest(json!({ "error": "This has no effect." })),
}
}
/// unblock a user
#[delete("/<target>/block")]
pub fn unblock_user(user: User, target: User) -> Response {
let col = database::get_collection("users");
match get_relationship(&user, &target) {
Relationship::Blocked => match get_relationship(&target, &user) {
Relationship::Blocked => {
if col
.update_one(
doc! {
"_id": user.id.clone(),
"relations.id": target.id.clone()
},
doc! {
"$set": {
"relations.$.status": Relationship::BlockedOther as i32
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
vec![user.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: user.id.clone(),
user: target.id.clone(),
status: Relationship::BlockedOther as i32,
}),
);
Response::Success(json!({ "status": Relationship::BlockedOther as i32 }))
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit to database, try again." }),
)
}
}
Relationship::BlockedOther => {
if col
.update_one(
doc! {
"_id": user.id.clone()
},
doc! {
"$pull": {
"relations": {
"id": target.id.clone()
}
}
},
None,
)
.is_ok()
{
if col
.update_one(
doc! {
"_id": target.id.clone()
},
doc! {
"$pull": {
"relations": {
"id": user.id.clone()
}
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
vec![user.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: user.id.clone(),
user: target.id.clone(),
status: Relationship::NONE as i32,
}),
);
notifications::send_message_threaded(
vec![target.id.clone()],
None,
Notification::user_friend_status(FriendStatus {
id: target.id.clone(),
user: user.id.clone(),
status: Relationship::NONE as i32,
}),
);
Response::Success(json!({ "status": Relationship::NONE as i32 }))
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit! Target remains in same state." }),
)
}
} else {
Response::InternalServerError(
json!({ "error": "Failed to commit to database, try again." }),
)
}
}
_ => unreachable!(),
},
Relationship::BlockedOther => {
Response::BadRequest(json!({ "error": "Cannot remove block by other user." }))
}
Relationship::Friend
| Relationship::Incoming
| Relationship::Outgoing
| Relationship::SELF
| Relationship::NONE => Response::BadRequest(json!({ "error": "This has no effect." })),
}
}
#[options("/<_target>")]
pub fn user_preflight(_target: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/query")]
pub fn query_preflight() -> Response {
Response::Result(super::Status::Ok)
}
#[options("/@me/dms")]
pub fn dms_preflight() -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/dm")]
pub fn dm_preflight(_target: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/friend")]
pub fn friend_preflight(_target: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/block")]
pub fn block_user_preflight(_target: String) -> Response {
Response::Result(super::Status::Ok)
}
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::try_join;
use mongodb::bson::doc;
use mongodb::options::{Collation, FindOneOptions};
use rocket_contrib::json::JsonValue;
#[put("/<username>/friend")]
pub async fn req(user: User, username: String) -> Result<JsonValue> {
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_id) {
RelationshipStatus::User => return Err(Error::NoEffect),
RelationshipStatus::Friend => return Err(Error::AlreadyFriends),
RelationshipStatus::Outgoing => return Err(Error::AlreadySentRequest),
RelationshipStatus::Blocked => return Err(Error::Blocked),
RelationshipStatus::BlockedOther => return Err(Error::BlockedByOther),
RelationshipStatus::Incoming => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id,
"relations._id": target_id
},
doc! {
"$set": {
"relations.$.status": "Friend"
}
},
None
),
col.update_one(
doc! {
"_id": target_id,
"relations._id": &user.id
},
doc! {
"$set": {
"relations.$.status": "Friend"
}
},
None
)
) {
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 {
operation: "update_one",
with: "user",
}),
}
}
RelationshipStatus::None => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id
},
doc! {
"$push": {
"relations": {
"_id": target_id,
"status": "Outgoing"
}
}
},
None
),
col.update_one(
doc! {
"_id": target_id
},
doc! {
"$push": {
"relations": {
"_id": &user.id,
"status": "Incoming"
}
}
},
None
)
) {
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 {
operation: "update_one",
with: "user",
}),
}
}
}
}
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::try_join;
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
#[put("/<target>/block")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let col = get_collection("users");
let target = target.fetch_user().await?;
match get_relationship(&user, &target.id) {
RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect),
RelationshipStatus::BlockedOther => {
col.update_one(
doc! {
"_id": &user.id,
"relations._id": &target.id
},
doc! {
"$set": {
"relations.$.status": "Blocked"
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone());
Ok(json!({ "status": "Blocked" }))
}
RelationshipStatus::None => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id
},
doc! {
"$push": {
"relations": {
"_id": &target.id,
"status": "Blocked"
}
}
},
None
),
col.update_one(
doc! {
"_id": &target.id
},
doc! {
"$push": {
"relations": {
"_id": &user.id,
"status": "BlockedOther"
}
}
},
None
)
) {
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 {
operation: "update_one",
with: "user",
}),
}
}
RelationshipStatus::Friend
| RelationshipStatus::Incoming
| RelationshipStatus::Outgoing => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id,
"relations._id": &target.id
},
doc! {
"$set": {
"relations.$.status": "Blocked"
}
},
None
),
col.update_one(
doc! {
"_id": &target.id,
"relations._id": &user.id
},
doc! {
"$set": {
"relations.$.status": "BlockedOther"
}
},
None
)
) {
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 {
operation: "update_one",
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::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
#[get("/dms")]
pub async fn req(user: User) -> Result<JsonValue> {
let mut cursor = get_collection("channels")
.find(
doc! {
"$or": [
{
"channel_type": "DirectMessage",
"active": true
},
{
"channel_type": "Group"
}
],
"recipients": user.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "channels",
})?;
let mut channels = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
channels.push(doc);
}
}
Ok(json!(channels))
}
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::*;
use crate::util::result::Result;
use rocket_contrib::json::JsonValue;
#[get("/<target>/relationship")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
Ok(json!({ "status": get_relationship(&user, &target.id) }))
}
use crate::database::*;
use crate::util::result::Result;
use rocket_contrib::json::JsonValue;
#[get("/relationships")]
pub async fn req(user: User) -> Result<JsonValue> {
Ok(if let Some(vec) = user.relations {
json!(vec)
} else {
json!([])
})
}
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket_contrib::json::JsonValue;
#[get("/<target>")]
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_access() {
Err(Error::MissingPermission)?
}
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!(),
}
}
use rocket::Route;
mod add_friend;
mod block_user;
mod change_username;
mod edit_user;
mod fetch_dms;
mod fetch_profile;
mod fetch_relationship;
mod fetch_relationships;
mod fetch_user;
mod find_mutual;
mod get_default_avatar;
mod open_dm;
mod remove_friend;
mod unblock_user;
pub fn routes() -> Vec<Route> {
routes![
// User Information
fetch_user::req,
edit_user::req,
change_username::req,
get_default_avatar::req,
fetch_profile::req,
// Direct Messaging
fetch_dms::req,
open_dm::req,
// Relationships
find_mutual::req,
fetch_relationships::req,
fetch_relationship::req,
add_friend::req,
remove_friend::req,
block_user::req,
unblock_user::req,
]
}
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
use ulid::Ulid;
#[get("/<target>/dm")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let query = if user.id == target.id {
doc! {
"channel_type": "SavedMessages",
"user": &user.id
}
} else {
doc! {
"channel_type": "DirectMessage",
"recipients": {
"$all": [ &user.id, &target.id ]
}
}
};
let existing_channel = get_collection("channels")
.find_one(query, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "channel",
})?;
if let Some(doc) = existing_channel {
Ok(json!(doc))
} else {
let id = Ulid::new().to_string();
let channel = if user.id == target.id {
Channel::SavedMessages { id, user: user.id }
} else {
Channel::DirectMessage {
id,
active: false,
recipients: vec![user.id, target.id],
last_message: None,
}
};
channel.clone().publish().await?;
Ok(json!(channel))
}
}
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::try_join;
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
#[delete("/<target>/friend")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let col = get_collection("users");
let target = target.fetch_user().await?;
match get_relationship(&user, &target.id) {
RelationshipStatus::Friend
| RelationshipStatus::Outgoing
| RelationshipStatus::Incoming => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id
},
doc! {
"$pull": {
"relations": {
"_id": &target.id
}
}
},
None
),
col.update_one(
doc! {
"_id": &target.id
},
doc! {
"$pull": {
"relations": {
"_id": &user.id
}
}
},
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 {
operation: "update_one",
with: "user",
}),
}
}
_ => Err(Error::NoEffect),
}
}
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::try_join;
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
#[delete("/<target>/block")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
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());
Ok(json!({ "status": "BlockedOther" }))
}
RelationshipStatus::BlockedOther => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id
},
doc! {
"$pull": {
"relations": {
"_id": &target.id
}
}
},
None
),
col.update_one(
doc! {
"_id": &target.id
},
doc! {
"$pull": {
"relations": {
"_id": &user.id
}
}
},
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: user,
status: RelationshipStatus::None,
}
.publish(target_id);
Ok(json!({ "status": "None" }))
}
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
}),
}
}
_ => Err(Error::InternalError),
},
_ => Err(Error::NoEffect),
}
}
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
#[derive(Serialize, Deserialize)]
struct CaptchaResponse {
success: bool,
}
pub fn verify(user_token: &Option<String>) -> Result<(), String> {
if let Ok(key) = env::var("HCAPTCHA_KEY") {
if let Some(token) = user_token {
let mut map = HashMap::new();
map.insert("secret", key);
map.insert("response", token.to_string());
let client = Client::new();
if let Ok(response) = client
.post("https://hcaptcha.com/siteverify")
.form(&map)
.send()
{
let result: CaptchaResponse = response
.json()
.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)
}