From 81f7527c79e7c31e30938781e76cae9757147e18 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 23 May 2025 20:32:43 +0200 Subject: [PATCH 1/3] feat: move image check to utils.rs --- src/structs.rs | 35 +++++++++-------------------------- src/utils.rs | 21 +++++++++++++++++++-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/structs.rs b/src/structs.rs index 371f623..b4403ed 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -5,9 +5,8 @@ use diesel_async::{pooled_connection::AsyncDieselConnectionManager, RunQueryDsl} use tokio::task; use url::Url; use actix_web::web::BytesMut; -use bindet::FileType; -use crate::{error::Error, Conn, Data, schema::*}; +use crate::{error::Error, schema::*, utils::image_check, Conn, Data}; fn load_or_empty(query_result: Result, diesel::result::Error>) -> Result, diesel::result::Error> { match query_result { @@ -256,7 +255,7 @@ impl GuildBuilder { uuid: self.uuid, name: self.name, description: self.description, - icon: self.icon, + icon: self.icon.and_then(|i| i.parse().ok()), owner_uuid: self.owner_uuid, roles: roles, member_count: member_count, @@ -269,7 +268,7 @@ pub struct Guild { pub uuid: Uuid, name: String, description: Option, - icon: Option, + icon: Option, owner_uuid: Uuid, pub roles: Vec, member_count: i64, @@ -410,29 +409,13 @@ impl Guild { // FIXME: Horrible security pub async fn set_icon(&mut self, bunny_cdn: &bunny_api_tokio::Client, conn: &mut Conn, cdn_url: Url, icon: BytesMut) -> Result<(), Error> { - let ico = icon.clone(); - - let image_type = task::spawn_blocking(move || { - let buf = std::io::Cursor::new(ico.to_vec()); - - let detect = bindet::detect(buf).map_err(|e| e.kind()); - - if let Ok(Some(file_type)) = detect { - if file_type.likely_to_be == vec![FileType::Jpg] { - return String::from("jpg") - } else if file_type.likely_to_be == vec![FileType::Png] { - return String::from("png") - } - } - String::from("unknown") - }).await?; - - if image_type == "unknown" { - return Err(Error::BadRequest("Not an image".to_string())) - } + let icon_clone = icon.clone(); + let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; if let Some(icon) = &self.icon { - let relative_url = icon.trim_start_matches("https://cdn.gorb.app/"); + let relative_url = icon + .path() + .trim_start_matches('/'); bunny_cdn.storage.delete(relative_url).await?; } @@ -450,7 +433,7 @@ impl Guild { .execute(conn) .await?; - self.icon = Some(icon_url.to_string()); + self.icon = Some(icon_url); Ok(()) } diff --git a/src/utils.rs b/src/utils.rs index b7ddcc1..4e9d435 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,8 @@ use actix_web::{ - cookie::{Cookie, SameSite, time::Duration}, - http::header::HeaderMap, + cookie::{time::Duration, Cookie, SameSite}, + http::header::HeaderMap, web::BytesMut, }; +use bindet::FileType; use getrandom::fill; use hex::encode; use redis::RedisError; @@ -59,6 +60,22 @@ pub fn generate_refresh_token() -> Result { Ok(encode(buf)) } +pub fn image_check(icon: BytesMut) -> Result { + let buf = std::io::Cursor::new(icon); + + let detect = bindet::detect(buf).map_err(|e| e.kind()); + + if let Ok(Some(file_type)) = detect { + if file_type.likely_to_be == vec![FileType::Jpg] { + return Ok(String::from("jpg")) + } else if file_type.likely_to_be == vec![FileType::Png] { + return Ok(String::from("png")) + } + } + + Err(Error::BadRequest("Uploaded file is not an image".to_string())) +} + impl Data { pub async fn set_cache_key( &self, From d6364a0dc007959f0b6e5e11b8537dcd13256594 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 23 May 2025 20:33:42 +0200 Subject: [PATCH 2/3] feat: add debug error printing Got a random error message while coding (still have no idea what sent it), this will let you run the code with debug logging if you arent sure where errors are coming from --- src/error.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 8843b22..47415c0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,7 +10,7 @@ use diesel_async::pooled_connection::PoolError as DieselPoolError; use tokio::task::JoinError; use serde_json::Error as JsonError; use toml::de::Error as TomlError; -use log::error; +use log::{debug, error}; #[derive(Debug, Error)] pub enum Error { @@ -54,6 +54,7 @@ pub enum Error { impl ResponseError for Error { fn error_response(&self) -> HttpResponse { + debug!("{:?}", self); error!("{}: {}", self.status_code(), self.to_string()); HttpResponse::build(self.status_code()) From 97072d54d180eedddd5b488b107a899bd9e86533 Mon Sep 17 00:00:00 2001 From: Radical Date: Fri, 23 May 2025 20:33:58 +0200 Subject: [PATCH 3/3] feat: user avatars --- Cargo.toml | 1 + src/api/v1/users/me.rs | 44 +++++++++++++++++++++++++++++------------ src/api/v1/users/mod.rs | 1 + src/structs.rs | 32 ++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e1f7a85..568466d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ diesel = { version = "2.2", features = ["uuid"] } diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" +actix-multipart = "0.7.2" [dependencies.tokio] version = "1.44" diff --git a/src/api/v1/users/me.rs b/src/api/v1/users/me.rs index 2cefa4f..9647002 100644 --- a/src/api/v1/users/me.rs +++ b/src/api/v1/users/me.rs @@ -1,4 +1,5 @@ use actix_web::{get, patch, web, HttpRequest, HttpResponse}; +use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm}; use serde::Deserialize; use crate::{error::Error, structs::Me, api::v1::auth::check_access_token, utils::get_auth_header, Data}; @@ -18,7 +19,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result, display_name: Option, @@ -26,8 +27,15 @@ struct NewInfo { email: Option, } +#[derive(Debug, MultipartForm)] +struct UploadForm { + #[multipart(limit = "100MB")] + avatar: Option, + json: Option>, +} + #[patch("/me")] -pub async fn update(req: HttpRequest, new_info: web::Json, data: web::Data) -> Result { +pub async fn update(req: HttpRequest, MultipartForm(form): MultipartForm, data: web::Data) -> Result { let headers = req.headers(); let auth_header = get_auth_header(headers)?; @@ -36,22 +44,32 @@ pub async fn update(req: HttpRequest, new_info: web::Json, data: web::D let uuid = check_access_token(auth_header, &mut conn).await?; - let me = Me::get(&mut conn, uuid).await?; + let mut me = Me::get(&mut conn, uuid).await?; - if let Some(username) = &new_info.username { - todo!(); + if let Some(avatar) = form.avatar { + let bytes = tokio::fs::read(avatar.file).await?; + + let byte_slice: &[u8] = &bytes; + + me.set_avatar(&data.bunny_cdn, &mut conn, data.config.bunny.cdn_url.clone(), byte_slice.into()).await?; } - if let Some(display_name) = &new_info.display_name { - todo!(); - } + if let Some(new_info) = form.json { + if let Some(username) = &new_info.username { + todo!(); + } - if let Some(password) = &new_info.password { - todo!(); - } + if let Some(display_name) = &new_info.display_name { + todo!(); + } - if let Some(email) = &new_info.email { - todo!(); + if let Some(password) = &new_info.password { + todo!(); + } + + if let Some(email) = &new_info.email { + todo!(); + } } Ok(HttpResponse::Ok().finish()) diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index 5259ed9..57f5f7d 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -9,6 +9,7 @@ pub fn web() -> Scope { web::scope("/users") .service(res) .service(me::res) + .service(me::update) .service(uuid::res) } diff --git a/src/structs.rs b/src/structs.rs index b4403ed..e593996 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -667,6 +667,38 @@ impl Me { Ok(me) } + + pub async fn set_avatar(&mut self, bunny_cdn: &bunny_api_tokio::Client, conn: &mut Conn, cdn_url: Url, avatar: BytesMut) -> Result<(), Error> { + let avatar_clone = avatar.clone(); + let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; + + if let Some(avatar) = &self.avatar { + let avatar_url: Url = avatar.parse()?; + + let relative_url = avatar_url + .path() + .trim_start_matches('/'); + + bunny_cdn.storage.delete(relative_url).await?; + } + + let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); + + bunny_cdn.storage.upload(path.clone(), avatar.into()).await?; + + let avatar_url = cdn_url.join(&path)?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::avatar.eq(avatar_url.as_str())) + .execute(conn) + .await?; + + self.avatar = Some(avatar_url.to_string()); + + Ok(()) + } } #[derive(Deserialize)]