Compare commits

...

3 commits

Author SHA1 Message Date
97072d54d1 feat: user avatars 2025-05-23 20:33:58 +02:00
d6364a0dc0 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
2025-05-23 20:33:42 +02:00
81f7527c79 feat: move image check to utils.rs 2025-05-23 20:32:43 +02:00
6 changed files with 95 additions and 42 deletions

View file

@ -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"

View file

@ -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<Data>) -> Result<HttpResponse
Ok(HttpResponse::Ok().json(me))
}
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
struct NewInfo {
username: Option<String>,
display_name: Option<String>,
@ -26,8 +27,15 @@ struct NewInfo {
email: Option<String>,
}
#[derive(Debug, MultipartForm)]
struct UploadForm {
#[multipart(limit = "100MB")]
avatar: Option<TempFile>,
json: Option<MpJson<NewInfo>>,
}
#[patch("/me")]
pub async fn update(req: HttpRequest, new_info: web::Json<NewInfo>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
pub async fn update(req: HttpRequest, MultipartForm(form): MultipartForm<UploadForm>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
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<NewInfo>, 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())

View file

@ -9,6 +9,7 @@ pub fn web() -> Scope {
web::scope("/users")
.service(res)
.service(me::res)
.service(me::update)
.service(uuid::res)
}

View file

@ -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())

View file

@ -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<T>(query_result: Result<Vec<T>, diesel::result::Error>) -> Result<Vec<T>, 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<String>,
icon: Option<String>,
icon: Option<Url>,
owner_uuid: Uuid,
pub roles: Vec<Role>,
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(())
}
@ -684,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)]

View file

@ -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<String, getrandom::Error> {
Ok(encode(buf))
}
pub fn image_check(icon: BytesMut) -> Result<String, Error> {
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,