From 15eb1027845e1a322f9f6d08387389226d6a3c3c Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 22:10:23 +0200 Subject: [PATCH 1/3] build: try to make dev bearable --- Cargo.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 492a284..30b5827 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,12 @@ strip = true lto = true codegen-units = 1 +# Speed up compilation to make dev bearable +[profile.dev] +debug = 0 +strip = "debuginfo" +codegen-units = 512 + [dependencies] actix-cors = "0.7.1" actix-web = "4.11" @@ -32,7 +38,7 @@ futures-util = "0.3.31" bunny-api-tokio = "0.3.0" bindet = "0.3.2" deadpool = "0.12" -diesel = { version = "2.2", features = ["uuid", "chrono"] } +diesel = { version = "2.2", features = ["uuid", "chrono"], default-features = false } diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" From 41defc4a252bf577b96d4d125f7a4335dc9d4b17 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 22:10:37 +0200 Subject: [PATCH 2/3] feat: add patch request to channels! --- src/api/v1/channels/uuid/mod.rs | 82 +++++++++++++++++++++++++- src/structs.rs | 101 +++++++++++++++++++++++++++++--- src/utils.rs | 3 + 3 files changed, 178 insertions(+), 8 deletions(-) diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index f429159..1874dfc 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -1,3 +1,5 @@ +//! `/api/v1/channels/{uuid}` Channel specific endpoints + pub mod messages; pub mod socket; @@ -8,8 +10,9 @@ use crate::{ structs::{Channel, Member}, utils::{get_auth_header, global_checks}, }; -use actix_web::{HttpRequest, HttpResponse, delete, get, web}; +use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; use uuid::Uuid; +use serde::Deserialize; #[get("/{uuid}")] pub async fn get( @@ -62,3 +65,80 @@ pub async fn delete( Ok(HttpResponse::Ok().finish()) } + +#[derive(Deserialize)] +struct NewInfo { + name: Option, + description: Option, + is_above: Option, +} + +/// `PATCH /api/v1/channels/{uuid}` Returns user with the given UUID +/// +/// requires auth: yes +/// +/// requires relation: yes +/// +/// ### Request Example +/// All fields are optional and can be nulled/dropped if only changing 1 value +/// ``` +/// json!({ +/// "name": "gaming-chat", +/// "description": "Gaming related topics.", +/// "is_above": "398f6d7b-752c-4348-9771-fe6024adbfb1" +/// }); +/// ``` +/// +/// ### Response Example +/// ``` +/// json!({ +/// uuid: "cdcac171-5add-4f88-9559-3a247c8bba2c", +/// guild_uuid: "383d2afa-082f-4dd3-9050-ca6ed91487b6", +/// name: "gaming-chat", +/// description: "Gaming related topics.", +/// is_above: "398f6d7b-752c-4348-9771-fe6024adbfb1", +/// permissions: { +/// role_uuid: "79cc0806-0f37-4a06-a468-6639c4311a2d", +/// permissions: 0 +/// } +/// }); +/// ``` +/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps +#[patch("/{uuid}")] +pub async fn patch( + req: HttpRequest, + path: web::Path<(Uuid,)>, + new_info: web::Json, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let channel_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let mut channel = Channel::fetch_one(&data, channel_uuid).await?; + + Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + + if let Some(new_name) = &new_info.name { + channel.set_name(&data, new_name.to_string()).await?; + } + + if let Some(new_description) = &new_info.description { + channel.set_description(&data, new_description.to_string()).await?; + } + + if let Some(new_is_above) = &new_info.is_above { + channel.set_description(&data, new_is_above.to_string()).await?; + } + + Ok(HttpResponse::Ok().json(channel)) +} + diff --git a/src/structs.rs b/src/structs.rs index 5e19dad..b955133 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -24,13 +24,9 @@ use url::Url; use uuid::Uuid; use crate::{ - Conn, Data, - error::Error, - schema::*, - utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_refresh_token, global_checks, - image_check, order_by_is_above, user_uuid_from_identifier, - }, + error::Error, schema::*, utils::{ + generate_refresh_token, global_checks, image_check, order_by_is_above, user_uuid_from_identifier, CHANNEL_REGEX, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX + }, Conn, Data }; pub trait HasUuid { @@ -231,6 +227,10 @@ impl Channel { name: String, description: Option, ) -> Result { + if !CHANNEL_REGEX.is_match(&name) { + return Err(Error::BadRequest("Channel name is invalid".to_string())) + } + let mut conn = data.pool.get().await?; let channel_uuid = Uuid::now_v7(); @@ -353,6 +353,93 @@ impl Channel { message.build(data).await } + + pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { + if !CHANNEL_REGEX.is_match(&new_name) { + return Err(Error::BadRequest("Channel name is invalid".to_string())) + } + + let mut conn = data.pool.get().await?; + + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::name.eq(&new_name)) + .execute(&mut conn) + .await?; + + self.name = new_name; + + Ok(()) + } + + pub async fn set_description(&mut self, data: &Data, new_description: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::description.eq(&new_description)) + .execute(&mut conn) + .await?; + + self.description = Some(new_description); + + Ok(()) + } + + pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + let old_above_uuid: Option = match dsl::channels + .filter(dsl::is_above.eq(self.uuid)) + .select(dsl::uuid) + .get_result(&mut conn) + .await + { + Ok(r) => Ok(Some(r)), + Err(e) if e == diesel::result::Error::NotFound => Ok(None), + Err(e) => Err(e), + }?; + + if let Some(uuid) = old_above_uuid { + update(channels::table) + .filter(dsl::uuid.eq(uuid)) + .set(dsl::is_above.eq(None::)) + .execute(&mut conn) + .await?; + } + + match update(channels::table) + .filter(dsl::is_above.eq(new_is_above)) + .set(dsl::is_above.eq(self.uuid)) + .execute(&mut conn) + .await + { + Ok(r) => Ok(r), + Err(e) if e == diesel::result::Error::NotFound => Ok(0), + Err(e) => Err(e), + }?; + + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::is_above.eq(new_is_above)) + .execute(&mut conn) + .await?; + + if let Some(uuid) = old_above_uuid { + update(channels::table) + .filter(dsl::uuid.eq(uuid)) + .set(dsl::is_above.eq(self.is_above)) + .execute(&mut conn) + .await?; + } + + self.is_above = Some(new_is_above); + + Ok(()) + } } #[derive(Clone, Copy)] diff --git a/src/utils.rs b/src/utils.rs index c9f3cb2..3bf7332 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -30,6 +30,9 @@ pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { pub static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); +pub static CHANNEL_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); + // Password is expected to be hashed using SHA3-384 pub static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); From c4fc23ec85f5da1422688279de9848b1e710abf8 Mon Sep 17 00:00:00 2001 From: Radical Date: Sun, 1 Jun 2025 22:20:29 +0200 Subject: [PATCH 3/3] feat: add about to users --- .../down.sql | 2 ++ .../up.sql | 2 ++ src/api/v1/me/mod.rs | 5 +++++ src/schema.rs | 2 ++ src/structs.rs | 21 +++++++++++++++++++ 5 files changed, 32 insertions(+) create mode 100644 migrations/2025-06-01-143713_add_about_to_users/down.sql create mode 100644 migrations/2025-06-01-143713_add_about_to_users/up.sql diff --git a/migrations/2025-06-01-143713_add_about_to_users/down.sql b/migrations/2025-06-01-143713_add_about_to_users/down.sql new file mode 100644 index 0000000..de48d07 --- /dev/null +++ b/migrations/2025-06-01-143713_add_about_to_users/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE users DROP COLUMN about; \ No newline at end of file diff --git a/migrations/2025-06-01-143713_add_about_to_users/up.sql b/migrations/2025-06-01-143713_add_about_to_users/up.sql new file mode 100644 index 0000000..54b5449 --- /dev/null +++ b/migrations/2025-06-01-143713_add_about_to_users/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE users ADD COLUMN about VARCHAR(200) DEFAULT NULL; diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index fc9e61b..ac35140 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -41,6 +41,7 @@ struct NewInfo { //password: Option, will probably be handled through a reset password link email: Option, pronouns: Option, + about: Option, } #[derive(Debug, MultipartForm)] @@ -102,5 +103,9 @@ pub async fn update( me.set_pronouns(&data, pronouns.clone()).await?; } + if let Some(about) = &form.json.about { + me.set_about(&data, about.clone()).await?; + } + Ok(HttpResponse::Ok().finish()) } diff --git a/src/schema.rs b/src/schema.rs index 3be885a..09ea7a3 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -146,6 +146,8 @@ diesel::table! { avatar -> Nullable, #[max_length = 32] pronouns -> Nullable, + #[max_length = 200] + about -> Nullable, } } diff --git a/src/structs.rs b/src/structs.rs index b955133..b68aaf5 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -953,6 +953,7 @@ pub struct User { display_name: Option, avatar: Option, pronouns: Option, + about: Option, } impl User { @@ -1004,6 +1005,7 @@ pub struct Me { display_name: Option, avatar: Option, pronouns: Option, + about: Option, email: String, pub email_verified: bool, } @@ -1198,6 +1200,25 @@ impl Me { Ok(()) } + + pub async fn set_about(&mut self, data: &Data, new_about: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(( + dsl::about.eq(new_about.as_str()), + )) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + Ok(()) + } } #[derive(Deserialize)]