chore(monorepo): delta, january, quark
This commit is contained in:
21
crates/delta/src/routes/users/add_friend.rs
Normal file
21
crates/delta/src/routes/users/add_friend.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use revolt_quark::models::User;
|
||||
use revolt_quark::{Database, Error, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
/// # Send Friend Request / Accept Request
|
||||
///
|
||||
/// Send a friend request to another user or accept another user's friend request.
|
||||
#[openapi(tag = "Relationships")]
|
||||
#[put("/<username>/friend")]
|
||||
pub async fn req(db: &State<Database>, user: User, username: String) -> Result<Json<User>> {
|
||||
let mut target = db.fetch_user_by_username(&username).await?;
|
||||
|
||||
if user.bot.is_some() || target.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
user.add_friend(db, &mut target).await?;
|
||||
Ok(Json(target.with_auto_perspective(db, &user).await))
|
||||
}
|
||||
16
crates/delta/src/routes/users/block_user.rs
Normal file
16
crates/delta/src/routes/users/block_user.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use revolt_quark::models::User;
|
||||
use revolt_quark::{Database, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
/// # Block User
|
||||
///
|
||||
/// Block another user by their id.
|
||||
#[openapi(tag = "Relationships")]
|
||||
#[put("/<target>/block")]
|
||||
pub async fn req(db: &State<Database>, user: User, target: String) -> Result<Json<User>> {
|
||||
let mut target = db.fetch_user(&target).await?;
|
||||
user.block_user(db, &mut target).await?;
|
||||
Ok(Json(target.with_auto_perspective(db, &user).await))
|
||||
}
|
||||
40
crates/delta/src/routes/users/change_username.rs
Normal file
40
crates/delta/src/routes/users/change_username.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use crate::util::regex::RE_USERNAME;
|
||||
use rauth::entities::Account;
|
||||
use revolt_quark::{models::User, Database, Error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Username Information
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataChangeUsername {
|
||||
/// New username
|
||||
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
|
||||
username: String,
|
||||
/// Current account password
|
||||
#[validate(length(min = 8, max = 1024))]
|
||||
password: String,
|
||||
}
|
||||
|
||||
/// # Change Username
|
||||
///
|
||||
/// Change your username.
|
||||
#[openapi(tag = "User Information")]
|
||||
#[patch("/@me/username", data = "<data>")]
|
||||
pub async fn req(
|
||||
db: &State<Database>,
|
||||
account: Account,
|
||||
mut user: User,
|
||||
data: Json<DataChangeUsername>,
|
||||
) -> Result<Json<User>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
account
|
||||
.verify_password(&data.password)
|
||||
.map_err(|_| Error::InvalidCredentials)?;
|
||||
|
||||
user.update_username(db, data.username).await?;
|
||||
Ok(Json(user.foreign()))
|
||||
}
|
||||
125
crates/delta/src/routes/users/edit_user.rs
Normal file
125
crates/delta/src/routes/users/edit_user.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use revolt_quark::models::user::{FieldsUser, PartialUser, User};
|
||||
use revolt_quark::models::File;
|
||||
use revolt_quark::{Database, Error, Result};
|
||||
|
||||
use revolt_quark::models::user::UserStatus;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Profile Data
|
||||
#[derive(Validate, Serialize, Deserialize, Debug, JsonSchema)]
|
||||
pub struct UserProfileData {
|
||||
/// Text to set as user profile description
|
||||
#[validate(length(min = 0, max = 2000))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
content: Option<String>,
|
||||
/// Attachment Id for background
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
background: Option<String>,
|
||||
}
|
||||
|
||||
/// # User Data
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataEditUser {
|
||||
/// New user status
|
||||
#[validate]
|
||||
status: Option<UserStatus>,
|
||||
/// New user profile data
|
||||
///
|
||||
/// This is applied as a partial.
|
||||
#[validate]
|
||||
profile: Option<UserProfileData>,
|
||||
/// Attachment Id for avatar
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
avatar: Option<String>,
|
||||
/// Fields to remove from user object
|
||||
#[validate(length(min = 1))]
|
||||
remove: Option<Vec<FieldsUser>>,
|
||||
}
|
||||
|
||||
/// # Edit User
|
||||
///
|
||||
/// Edit currently authenticated user.
|
||||
#[openapi(tag = "User Information")]
|
||||
#[patch("/@me", data = "<data>")]
|
||||
pub async fn req(
|
||||
db: &State<Database>,
|
||||
mut user: User,
|
||||
data: Json<DataEditUser>,
|
||||
) -> Result<Json<User>> {
|
||||
let 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(Json(user));
|
||||
}
|
||||
|
||||
// 1. Remove fields from object
|
||||
if let Some(fields) = &data.remove {
|
||||
if fields.contains(&FieldsUser::Avatar) {
|
||||
if let Some(avatar) = &user.avatar {
|
||||
db.mark_attachment_as_deleted(&avatar.id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if fields.contains(&FieldsUser::ProfileBackground) {
|
||||
if let Some(profile) = &user.profile {
|
||||
if let Some(background) = &profile.background {
|
||||
db.mark_attachment_as_deleted(&background.id).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for field in fields {
|
||||
user.remove(field);
|
||||
}
|
||||
}
|
||||
|
||||
let mut partial: PartialUser = Default::default();
|
||||
|
||||
// 2. Apply new avatar
|
||||
if let Some(avatar) = data.avatar {
|
||||
partial.avatar = Some(File::use_avatar(db, &avatar, &user.id).await?);
|
||||
}
|
||||
|
||||
// 3. Apply new status
|
||||
if let Some(status) = data.status {
|
||||
let mut new_status = user.status.take().unwrap_or_default();
|
||||
if let Some(text) = status.text {
|
||||
new_status.text = Some(text);
|
||||
}
|
||||
|
||||
if let Some(presence) = status.presence {
|
||||
new_status.presence = Some(presence);
|
||||
}
|
||||
|
||||
partial.status = Some(new_status);
|
||||
}
|
||||
|
||||
// 4. Apply new profile
|
||||
if let Some(profile) = data.profile {
|
||||
let mut new_profile = user.profile.take().unwrap_or_default();
|
||||
if let Some(content) = profile.content {
|
||||
new_profile.content = Some(content);
|
||||
}
|
||||
|
||||
if let Some(background) = profile.background {
|
||||
new_profile.background = Some(File::use_background(db, &background, &user.id).await?);
|
||||
}
|
||||
|
||||
partial.profile = Some(new_profile);
|
||||
}
|
||||
|
||||
user.update(db, partial, data.remove.unwrap_or_default())
|
||||
.await?;
|
||||
|
||||
Ok(Json(user.foreign()))
|
||||
}
|
||||
15
crates/delta/src/routes/users/fetch_dms.rs
Normal file
15
crates/delta/src/routes/users/fetch_dms.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
Database, Result,
|
||||
};
|
||||
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Fetch Direct Message Channels
|
||||
///
|
||||
/// This fetches your direct messages, including any DM and group DM conversations.
|
||||
#[openapi(tag = "Direct Messaging")]
|
||||
#[get("/dms")]
|
||||
pub async fn req(db: &State<Database>, user: User) -> Result<Json<Vec<Channel>>> {
|
||||
db.find_direct_messages(&user.id).await.map(Json)
|
||||
}
|
||||
28
crates/delta/src/routes/users/fetch_profile.rs
Normal file
28
crates/delta/src/routes/users/fetch_profile.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use revolt_quark::{
|
||||
models::{user::UserProfile, User},
|
||||
perms, Database, Error, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Fetch User Profile
|
||||
///
|
||||
/// Retrieve a user's profile data.
|
||||
///
|
||||
/// Will fail if you do not have permission to access the other user's profile.
|
||||
#[openapi(tag = "User Information")]
|
||||
#[get("/<target>/profile")]
|
||||
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<UserProfile>> {
|
||||
let target = target.as_user(db).await?;
|
||||
|
||||
if perms(&user)
|
||||
.user(&target)
|
||||
.calc_user(db)
|
||||
.await
|
||||
.get_view_profile()
|
||||
{
|
||||
Ok(Json(target.profile.unwrap_or_default()))
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
13
crates/delta/src/routes/users/fetch_self.rs
Executable file
13
crates/delta/src/routes/users/fetch_self.rs
Executable file
@@ -0,0 +1,13 @@
|
||||
use revolt_quark::models::User;
|
||||
use revolt_quark::Result;
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Fetch Self
|
||||
///
|
||||
/// Retrieve your user information.
|
||||
#[openapi(tag = "User Information")]
|
||||
#[get("/@me")]
|
||||
pub async fn req(user: User) -> Result<Json<User>> {
|
||||
Ok(Json(user.foreign()))
|
||||
}
|
||||
23
crates/delta/src/routes/users/fetch_user.rs
Normal file
23
crates/delta/src/routes/users/fetch_user.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use revolt_quark::{models::User, perms, Database, Error, Ref, Result};
|
||||
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Fetch User
|
||||
///
|
||||
/// Retrieve a user's information.
|
||||
#[openapi(tag = "User Information")]
|
||||
#[get("/<target>")]
|
||||
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<User>> {
|
||||
if target.id == user.id {
|
||||
return Ok(Json(user));
|
||||
}
|
||||
|
||||
let target = target.as_user(db).await?;
|
||||
|
||||
let permissions = perms(&user).user(&target).calc_user(db).await;
|
||||
if permissions.get_access() {
|
||||
Ok(Json(target.with_perspective(&user, &permissions)))
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
46
crates/delta/src/routes/users/find_mutual.rs
Normal file
46
crates/delta/src/routes/users/find_mutual.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use revolt_quark::models::User;
|
||||
use revolt_quark::{perms, Database, Error, Ref, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use serde::Serialize;
|
||||
|
||||
/// # Mutual Friends and Servers Response
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
pub struct MutualResponse {
|
||||
/// Array of mutual user IDs that both users are friends with
|
||||
users: Vec<String>,
|
||||
/// Array of mutual server IDs that both users are in
|
||||
servers: Vec<String>,
|
||||
}
|
||||
|
||||
/// # Fetch Mutual Friends And Servers
|
||||
///
|
||||
/// Retrieve a list of mutual friends and servers with another user.
|
||||
#[openapi(tag = "Relationships")]
|
||||
#[get("/<target>/mutual")]
|
||||
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<MutualResponse>> {
|
||||
if target.id == user.id {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let target = target.as_user(db).await?;
|
||||
|
||||
if perms(&user)
|
||||
.user(&target)
|
||||
.calc_user(db)
|
||||
.await
|
||||
.get_view_profile()
|
||||
{
|
||||
Ok(Json(MutualResponse {
|
||||
users: db.fetch_mutual_user_ids(&user.id, &target.id).await?,
|
||||
servers: db.fetch_mutual_server_ids(&user.id, &target.id).await?,
|
||||
}))
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
76
crates/delta/src/routes/users/get_default_avatar.rs
Normal file
76
crates/delta/src/routes/users/get_default_avatar.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use rocket::http::ContentType;
|
||||
use rocket::response::{self, Responder};
|
||||
use rocket::{Request, Response};
|
||||
use rocket_okapi::okapi::openapi3::{self, MediaType, RefOr};
|
||||
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
|
||||
|
||||
pub struct CachedFile((ContentType, Vec<u8>));
|
||||
|
||||
pub static CACHE_CONTROL: &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()
|
||||
}
|
||||
}
|
||||
|
||||
impl rocket_okapi::response::OpenApiResponderInner for CachedFile {
|
||||
fn responses(
|
||||
_gen: &mut rocket_okapi::gen::OpenApiGenerator,
|
||||
) -> std::result::Result<openapi3::Responses, rocket_okapi::OpenApiError> {
|
||||
let mut responses = schemars::Map::new();
|
||||
let mut content = schemars::Map::new();
|
||||
|
||||
content.insert(
|
||||
"image/png".to_owned(),
|
||||
MediaType {
|
||||
schema: Some(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
||||
format: Some("binary".to_owned()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
responses.insert(
|
||||
"200".to_string(),
|
||||
RefOr::Object(openapi3::Response {
|
||||
description: "Default Avatar Picture".to_string(),
|
||||
content,
|
||||
..Default::default()
|
||||
}),
|
||||
);
|
||||
|
||||
Ok(openapi3::Responses {
|
||||
responses,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// # Fetch Default Avatar
|
||||
///
|
||||
/// This returns a default avatar based on the given id.
|
||||
#[openapi(tag = "User Information")]
|
||||
#[get("/<target>/default_avatar")]
|
||||
pub async fn req(target: String) -> CachedFile {
|
||||
CachedFile((
|
||||
ContentType::PNG,
|
||||
match target.chars().last().unwrap() {
|
||||
// 0123456789ABCDEFGHJKMNPQRSTVWXYZ
|
||||
'0' | '1' | '2' | '3' | 'S' | 'Z' => {
|
||||
include_bytes!(crate::asset!("user/2.png")).to_vec()
|
||||
}
|
||||
'4' | '5' | '6' | '7' | 'T' => include_bytes!(crate::asset!("user/3.png")).to_vec(),
|
||||
'8' | '9' | 'A' | 'B' => include_bytes!(crate::asset!("user/4.png")).to_vec(),
|
||||
'C' | 'D' | 'E' | 'F' | 'V' => include_bytes!(crate::asset!("user/5.png")).to_vec(),
|
||||
'G' | 'H' | 'J' | 'K' | 'W' => include_bytes!(crate::asset!("user/6.png")).to_vec(),
|
||||
'M' | 'N' | 'P' | 'Q' | 'X' => include_bytes!(crate::asset!("user/7.png")).to_vec(),
|
||||
/*'0' | '1' | '2' | '3' | 'R' | 'Y'*/
|
||||
_ => include_bytes!(crate::asset!("user/1.png")).to_vec(),
|
||||
},
|
||||
))
|
||||
}
|
||||
37
crates/delta/src/routes/users/mod.rs
Normal file
37
crates/delta/src/routes/users/mod.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod add_friend;
|
||||
mod block_user;
|
||||
mod change_username;
|
||||
mod edit_user;
|
||||
mod fetch_dms;
|
||||
mod fetch_profile;
|
||||
mod fetch_self;
|
||||
mod fetch_user;
|
||||
mod find_mutual;
|
||||
mod get_default_avatar;
|
||||
mod open_dm;
|
||||
mod remove_friend;
|
||||
mod unblock_user;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
// User Information
|
||||
fetch_self::req,
|
||||
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,
|
||||
add_friend::req,
|
||||
remove_friend::req,
|
||||
block_user::req,
|
||||
unblock_user::req,
|
||||
]
|
||||
}
|
||||
55
crates/delta/src/routes/users/open_dm.rs
Normal file
55
crates/delta/src/routes/users/open_dm.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
perms, Database, Error, Ref, Result, UserPermission,
|
||||
};
|
||||
|
||||
use rocket::{serde::json::Json, State};
|
||||
use ulid::Ulid;
|
||||
|
||||
/// # Open Direct Message
|
||||
///
|
||||
/// Open a DM with another user.
|
||||
///
|
||||
/// If the target is oneself, a saved messages channel is returned.
|
||||
#[openapi(tag = "Direct Messaging")]
|
||||
#[get("/<target>/dm")]
|
||||
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<Channel>> {
|
||||
let target = target.as_user(db).await?;
|
||||
|
||||
// If the target is oneself, open saved messages.
|
||||
if target.id == user.id {
|
||||
return if let Ok(channel) = db.find_direct_message_channel(&user.id, &target.id).await {
|
||||
Ok(Json(channel))
|
||||
} else {
|
||||
let new_channel = Channel::SavedMessages {
|
||||
id: Ulid::new().to_string(),
|
||||
user: user.id,
|
||||
};
|
||||
|
||||
new_channel.create(db).await?;
|
||||
Ok(Json(new_channel))
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise try to find or create a DM.
|
||||
if let Ok(channel) = db.find_direct_message_channel(&user.id, &target.id).await {
|
||||
Ok(Json(channel))
|
||||
} else if perms(&user)
|
||||
.user(&target)
|
||||
.calc_user(db)
|
||||
.await
|
||||
.get_send_message()
|
||||
{
|
||||
let new_channel = Channel::DirectMessage {
|
||||
id: Ulid::new().to_string(),
|
||||
active: false,
|
||||
recipients: vec![user.id, target.id],
|
||||
last_message_id: None,
|
||||
};
|
||||
|
||||
new_channel.create(db).await?;
|
||||
Ok(Json(new_channel))
|
||||
} else {
|
||||
Error::from_user_permission(UserPermission::SendMessage)
|
||||
}
|
||||
}
|
||||
21
crates/delta/src/routes/users/remove_friend.rs
Normal file
21
crates/delta/src/routes/users/remove_friend.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use revolt_quark::models::User;
|
||||
use revolt_quark::{Database, Error, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
/// # Deny Friend Request / Remove Friend
|
||||
///
|
||||
/// Denies another user's friend request or removes an existing friend.
|
||||
#[openapi(tag = "Relationships")]
|
||||
#[delete("/<target>/friend")]
|
||||
pub async fn req(db: &State<Database>, user: User, target: String) -> Result<Json<User>> {
|
||||
let mut target = db.fetch_user(&target).await?;
|
||||
|
||||
if user.bot.is_some() || target.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
user.remove_friend(db, &mut target).await?;
|
||||
Ok(Json(target.with_auto_perspective(db, &user).await))
|
||||
}
|
||||
16
crates/delta/src/routes/users/unblock_user.rs
Normal file
16
crates/delta/src/routes/users/unblock_user.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use revolt_quark::models::User;
|
||||
use revolt_quark::{Database, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
|
||||
/// # Unblock User
|
||||
///
|
||||
/// Unblock another user by their id.
|
||||
#[openapi(tag = "Relationships")]
|
||||
#[delete("/<target>/block")]
|
||||
pub async fn req(db: &State<Database>, user: User, target: String) -> Result<Json<User>> {
|
||||
let mut target = db.fetch_user(&target).await?;
|
||||
user.unblock_user(db, &mut target).await?;
|
||||
Ok(Json(target.with_auto_perspective(db, &user).await))
|
||||
}
|
||||
Reference in New Issue
Block a user