diff --git a/src/api/v1/servers/uuid/icon.rs b/src/api/v1/servers/uuid/icon.rs new file mode 100644 index 0000000..e8a828d --- /dev/null +++ b/src/api/v1/servers/uuid/icon.rs @@ -0,0 +1,56 @@ +use actix_web::{put, web, Error, HttpRequest, HttpResponse}; +use uuid::Uuid; +use futures_util::StreamExt as _; + +use crate::{api::v1::auth::check_access_token, structs::{Guild, Member}, utils::get_auth_header, Data}; + +#[put("{uuid}/icon")] +pub async fn upload( + req: HttpRequest, + path: web::Path<(Uuid,)>, + mut payload: web::Payload, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers); + + if let Err(error) = auth_header { + return Ok(error); + } + + let guild_uuid = path.into_inner().0; + + let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; + + if let Err(error) = authorized { + return Ok(error); + } + + let uuid = authorized.unwrap(); + + let member = Member::fetch_one(&data.pool, uuid, guild_uuid).await; + + if let Err(error) = member { + return Ok(error); + } + + let guild_result = Guild::fetch_one(&data.pool, guild_uuid).await; + + if let Err(error) = guild_result { + return Ok(error); + } + + let mut guild = guild_result.unwrap(); + + let mut bytes = web::BytesMut::new(); + while let Some(item) = payload.next().await { + bytes.extend_from_slice(&item?); + } + + if let Err(error) = guild.set_icon(&data.bunny_cdn, &data.pool, data.config.bunny.cdn_url.clone(), bytes).await { + return Ok(error) + } + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/api/v1/servers/uuid/mod.rs b/src/api/v1/servers/uuid/mod.rs index 8f387aa..87d9e51 100644 --- a/src/api/v1/servers/uuid/mod.rs +++ b/src/api/v1/servers/uuid/mod.rs @@ -4,6 +4,7 @@ use uuid::Uuid; mod channels; mod invites; mod roles; +mod icon; use crate::{ Data, @@ -30,6 +31,8 @@ pub fn web() -> Scope { // Invites .service(invites::get) .service(invites::create) + // Icon + .service(icon::upload) } #[get("/{uuid}")] diff --git a/src/structs.rs b/src/structs.rs index 0ef738f..92252cc 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -1,9 +1,12 @@ use std::str::FromStr; -use actix_web::HttpResponse; +use actix_web::{web::BytesMut, HttpResponse}; +use bindet::FileType; use log::error; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Postgres, prelude::FromRow}; +use tokio::task; +use url::Url; use uuid::Uuid; use crate::Data; @@ -288,7 +291,7 @@ pub struct Guild { pub uuid: Uuid, name: String, description: Option, - icon: String, + icon: Option, owner_uuid: Uuid, pub roles: Vec, member_count: i64, @@ -297,7 +300,7 @@ pub struct Guild { impl Guild { pub async fn fetch_one(pool: &Pool, guild_uuid: Uuid) -> Result { let row = sqlx::query_as(&format!( - "SELECT CAST(owner_uuid AS VARCHAR), name, description FROM guilds WHERE uuid = '{}'", + "SELECT CAST(owner_uuid AS VARCHAR), name, description, icon FROM guilds WHERE uuid = '{}'", guild_uuid )) .fetch_one(pool) @@ -309,7 +312,7 @@ impl Guild { return Err(HttpResponse::InternalServerError().finish()); } - let (owner_uuid_raw, name, description): (String, String, Option) = row.unwrap(); + let (owner_uuid_raw, name, description, icon): (String, String, Option, Option) = row.unwrap(); let owner_uuid = Uuid::from_str(&owner_uuid_raw).unwrap(); @@ -321,8 +324,7 @@ impl Guild { uuid: guild_uuid, name, description, - // FIXME: This isnt supposed to be bogus - icon: String::from("bogus"), + icon, owner_uuid, roles, member_count, @@ -378,7 +380,7 @@ impl Guild { uuid: guild_uuid, name, description, - icon: "bogus".to_string(), + icon: None, owner_uuid, roles: vec![], member_count: 1, @@ -443,6 +445,78 @@ impl Guild { guild_uuid: self.uuid, }) } + + // FIXME: Horrible security + pub async fn set_icon(&mut self, bunny_cdn: &bunny_api_tokio::Client, pool: &Pool, cdn_url: Url, icon: BytesMut) -> Result<(), HttpResponse> { + let ico = icon.clone(); + + let result = 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 let Err(error) = result { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + let image_type = result.unwrap(); + + if image_type == "unknown" { + return Err(HttpResponse::BadRequest().finish()) + } + + if let Some(icon) = &self.icon { + let relative_url = icon.trim_start_matches("https://cdn.gorb.app/"); + + let delete_result = bunny_cdn.storage.delete(relative_url).await; + + if let Err(error) = delete_result { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + } + + let path = format!("icons/{}/icon.{}", self.uuid, image_type); + + let upload_result = bunny_cdn.storage.upload(path.clone(), icon.into()).await; + + if let Err(error) = upload_result { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + + let icon_url = cdn_url.join(&path).unwrap(); + + let row = sqlx::query(&format!("UPDATE guilds SET icon = $1 WHERE uuid = '{}'", self.uuid)) + .bind(icon_url.as_str()) + .execute(pool) + .await; + + if let Err(error) = row { + error!("{}", error); + + return Err(HttpResponse::InternalServerError().finish()) + } + + self.icon = Some(icon_url.to_string()); + + Ok(()) + } } #[derive(FromRow)]