Compare commits

...

11 commits

Author SHA1 Message Date
860fa7a66e Merge pull request 'feat: Bunny CDN integration for images' (#17) from wip/images into main
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
Reviewed-on: #17
2025-05-23 23:06:21 +00:00
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
149b81973d Merge branch 'main' into wip/images 2025-05-23 13:45:17 +02:00
f655ced060 Merge branch 'main' into wip/images 2025-05-20 22:53:13 +02:00
85f6db499f fix: use patch request for updating user 2025-05-20 22:20:45 +02:00
4124b08bb2 style: change function name 2025-05-20 22:20:32 +02:00
b66c8f0613 feat: implement proper user and me structs 2025-05-20 18:04:44 +02:00
cee1b41e89 feat: implement server icons! 2025-05-20 14:54:47 +02:00
cf333b4eba feat: add bunny-api-tokio 2025-05-20 14:54:34 +02:00
16 changed files with 334 additions and 86 deletions

View file

@ -29,11 +29,14 @@ uuid = { version = "1.16", features = ["serde", "v7"] }
random-string = "1.1" random-string = "1.1"
actix-ws = "0.3.0" actix-ws = "0.3.0"
futures-util = "0.3.31" futures-util = "0.3.31"
bunny-api-tokio = "0.2.1"
bindet = "0.3.2"
deadpool = "0.12" deadpool = "0.12"
diesel = { version = "2.2", features = ["uuid"] } diesel = { version = "2.2", features = ["uuid"] }
diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] } diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] }
diesel_migrations = { version = "2.2.0", features = ["postgres"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] }
thiserror = "2.0.12" thiserror = "2.0.12"
actix-multipart = "0.7.2"
[dependencies.tokio] [dependencies.tokio]
version = "1.44" version = "1.44"

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE guilds DROP COLUMN icon;

View file

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE guilds ADD COLUMN icon VARCHAR(100) DEFAULT NULL;

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE users DROP COLUMN avatar;

View file

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE users ADD COLUMN avatar varchar(100) DEFAULT NULL;

View file

@ -0,0 +1,36 @@
use actix_web::{put, web, HttpRequest, HttpResponse};
use uuid::Uuid;
use futures_util::StreamExt as _;
use crate::{error::Error, 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<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
Member::fetch_one(&mut conn, uuid, guild_uuid).await?;
let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?;
let mut bytes = web::BytesMut::new();
while let Some(item) = payload.next().await {
bytes.extend_from_slice(&item?);
}
guild.set_icon(&data.bunny_cdn, &mut conn, data.config.bunny.cdn_url.clone(), bytes).await?;
Ok(HttpResponse::Ok().finish())
}

View file

@ -4,6 +4,7 @@ use uuid::Uuid;
mod channels; mod channels;
mod invites; mod invites;
mod roles; mod roles;
mod icon;
use crate::{ use crate::{
error::Error, error::Error,
@ -31,6 +32,8 @@ pub fn web() -> Scope {
// Invites // Invites
.service(invites::get) .service(invites::get)
.service(invites::create) .service(invites::create)
// Icon
.service(icon::upload)
} }
#[get("/{uuid}")] #[get("/{uuid}")]

View file

@ -1,20 +1,8 @@
use actix_web::{HttpRequest, HttpResponse, get, web}; use actix_web::{get, patch, web, HttpRequest, HttpResponse};
use diesel::{prelude::Queryable, ExpressionMethods, QueryDsl, Selectable, SelectableHelper}; use actix_multipart::form::{json::Json as MpJson, tempfile::TempFile, MultipartForm};
use diesel_async::RunQueryDsl; use serde::Deserialize;
use log::error;
use serde::Serialize;
use uuid::Uuid;
use crate::{error::Error, api::v1::auth::check_access_token, schema::users::{self, dsl}, utils::get_auth_header, Data}; use crate::{error::Error, structs::Me, api::v1::auth::check_access_token, utils::get_auth_header, Data};
#[derive(Serialize, Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct Response {
uuid: Uuid,
username: String,
display_name: Option<String>,
}
#[get("/me")] #[get("/me")]
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
@ -26,16 +14,63 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
let uuid = check_access_token(auth_header, &mut conn).await?; let uuid = check_access_token(auth_header, &mut conn).await?;
let user: Result<Response, diesel::result::Error> = dsl::users let me = Me::get(&mut conn, uuid).await?;
.filter(dsl::uuid.eq(uuid))
.select(Response::as_select())
.get_result(&mut conn)
.await;
if let Err(error) = user { Ok(HttpResponse::Ok().json(me))
error!("{}", error); }
return Ok(HttpResponse::InternalServerError().finish())
#[derive(Debug, Deserialize)]
struct NewInfo {
username: Option<String>,
display_name: Option<String>,
password: Option<String>,
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, MultipartForm(form): MultipartForm<UploadForm>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let mut me = Me::get(&mut conn, uuid).await?;
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?;
} }
Ok(HttpResponse::Ok().json(user.unwrap())) if let Some(new_info) = form.json {
if let Some(username) = &new_info.username {
todo!();
}
if let Some(display_name) = &new_info.display_name {
todo!();
}
if let Some(password) = &new_info.password {
todo!();
}
if let Some(email) = &new_info.email {
todo!();
}
}
Ok(HttpResponse::Ok().finish())
} }

View file

@ -1,28 +1,15 @@
use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use actix_web::{HttpRequest, HttpResponse, Scope, get, web};
use diesel::{prelude::Queryable, QueryDsl, Selectable, SelectableHelper};
use diesel_async::RunQueryDsl;
use serde::Serialize;
use ::uuid::Uuid;
use crate::{error::Error,api::v1::auth::check_access_token, schema::users::{self, dsl}, structs::StartAmountQuery, utils::get_auth_header, Data}; use crate::{api::v1::auth::check_access_token, error::Error, structs::{StartAmountQuery, User}, utils::get_auth_header, Data};
mod me; mod me;
mod uuid; mod uuid;
#[derive(Serialize, Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct Response {
uuid: Uuid,
username: String,
display_name: Option<String>,
email: String,
}
pub fn web() -> Scope { pub fn web() -> Scope {
web::scope("/users") web::scope("/users")
.service(res) .service(res)
.service(me::res) .service(me::res)
.service(me::update)
.service(uuid::res) .service(uuid::res)
} }
@ -48,13 +35,7 @@ pub async fn res(
check_access_token(auth_header, &mut conn).await?; check_access_token(auth_header, &mut conn).await?;
let users: Vec<Response> = dsl::users let users = User::fetch_amount(&mut conn, start, amount).await?;
.order_by(dsl::username)
.offset(start)
.limit(amount)
.select(Response::as_select())
.load(&mut conn)
.await?;
Ok(HttpResponse::Ok().json(users)) Ok(HttpResponse::Ok().json(users))
} }

View file

@ -1,20 +1,8 @@
use actix_web::{HttpRequest, HttpResponse, get, web}; use actix_web::{HttpRequest, HttpResponse, get, web};
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper};
use diesel_async::RunQueryDsl;
use log::error;
use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{error::Error, api::v1::auth::check_access_token, schema::users::{self, dsl}, utils::get_auth_header, Data}; use crate::{error::Error, api::v1::auth::check_access_token, structs::User, utils::get_auth_header, Data};
#[derive(Serialize, Queryable, Selectable, Clone)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct Response {
uuid: Uuid,
username: String,
display_name: Option<String>,
}
#[get("/{uuid}")] #[get("/{uuid}")]
pub async fn res( pub async fn res(
@ -32,28 +20,17 @@ pub async fn res(
check_access_token(auth_header, &mut conn).await?; check_access_token(auth_header, &mut conn).await?;
let cache_result = data.get_cache_key(uuid.to_string()).await; if let Ok(cache_hit) = data.get_cache_key(uuid.to_string()).await {
if let Ok(cache_hit) = cache_result {
return Ok(HttpResponse::Ok() return Ok(HttpResponse::Ok()
.content_type("application/json") .content_type("application/json")
.body(cache_hit)); .body(cache_hit));
} }
let user: Response = dsl::users let user = User::fetch_one(&mut conn, uuid).await?;
.filter(dsl::uuid.eq(uuid))
.select(Response::as_select())
.get_result(&mut conn)
.await?;
let cache_result = data data
.set_cache_key(uuid.to_string(), user.clone(), 1800) .set_cache_key(uuid.to_string(), user.clone(), 1800)
.await; .await?;
if let Err(error) = cache_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(user)) Ok(HttpResponse::Ok().json(user))
} }

View file

@ -1,13 +1,16 @@
use bunny_api_tokio::edge_storage::Endpoint;
use crate::error::Error; use crate::error::Error;
use log::debug; use log::debug;
use serde::Deserialize; use serde::Deserialize;
use tokio::fs::read_to_string; use tokio::fs::read_to_string;
use url::Url;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ConfigBuilder { pub struct ConfigBuilder {
database: Database, database: Database,
cache_database: CacheDatabase, cache_database: CacheDatabase,
web: Option<WebBuilder>, web: Option<WebBuilder>,
bunny: BunnyBuilder,
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
@ -35,6 +38,14 @@ struct WebBuilder {
_ssl: Option<bool>, _ssl: Option<bool>,
} }
#[derive(Debug, Deserialize)]
struct BunnyBuilder {
api_key: String,
endpoint: String,
storage_zone: String,
cdn_url: Url,
}
impl ConfigBuilder { impl ConfigBuilder {
pub async fn load(path: String) -> Result<Self, Error> { pub async fn load(path: String) -> Result<Self, Error> {
debug!("loading config from: {}", path); debug!("loading config from: {}", path);
@ -58,10 +69,31 @@ impl ConfigBuilder {
} }
}; };
let endpoint = match &*self.bunny.endpoint {
"Frankfurt" => Endpoint::Frankfurt,
"London" => Endpoint::London,
"New York" => Endpoint::NewYork,
"Los Angeles" => Endpoint::LosAngeles,
"Singapore" => Endpoint::Singapore,
"Stockholm" => Endpoint::Stockholm,
"Sao Paulo" => Endpoint::SaoPaulo,
"Johannesburg" => Endpoint::Johannesburg,
"Sydney" => Endpoint::Sydney,
url => Endpoint::Custom(url.to_string()),
};
let bunny = Bunny {
api_key: self.bunny.api_key,
endpoint,
storage_zone: self.bunny.storage_zone,
cdn_url: self.bunny.cdn_url,
};
Config { Config {
database: self.database, database: self.database,
cache_database: self.cache_database, cache_database: self.cache_database,
web, web,
bunny,
} }
} }
} }
@ -71,6 +103,7 @@ pub struct Config {
pub database: Database, pub database: Database,
pub cache_database: CacheDatabase, pub cache_database: CacheDatabase,
pub web: Web, pub web: Web,
pub bunny: Bunny,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -79,6 +112,14 @@ pub struct Web {
pub port: u16, pub port: u16,
} }
#[derive(Debug, Clone)]
pub struct Bunny {
pub api_key: String,
pub endpoint: Endpoint,
pub storage_zone: String,
pub cdn_url: Url,
}
impl Database { impl Database {
pub fn url(&self) -> String { pub fn url(&self) -> String {
let mut url = String::from("postgres://"); let mut url = String::from("postgres://");

View file

@ -1,6 +1,6 @@
use std::{io, time::SystemTimeError}; use std::{io, time::SystemTimeError};
use actix_web::{error::ResponseError, http::{header::{ContentType, ToStrError}, StatusCode}, HttpResponse}; use actix_web::{error::{PayloadError, ResponseError}, http::{header::{ContentType, ToStrError}, StatusCode}, HttpResponse};
use deadpool::managed::{BuildError, PoolError}; use deadpool::managed::{BuildError, PoolError};
use redis::RedisError; use redis::RedisError;
use serde::Serialize; use serde::Serialize;
@ -10,7 +10,7 @@ use diesel_async::pooled_connection::PoolError as DieselPoolError;
use tokio::task::JoinError; use tokio::task::JoinError;
use serde_json::Error as JsonError; use serde_json::Error as JsonError;
use toml::de::Error as TomlError; use toml::de::Error as TomlError;
use log::error; use log::{debug, error};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
@ -38,6 +38,12 @@ pub enum Error {
ToStrError(#[from] ToStrError), ToStrError(#[from] ToStrError),
#[error(transparent)] #[error(transparent)]
RandomError(#[from] getrandom::Error), RandomError(#[from] getrandom::Error),
#[error(transparent)]
BunnyError(#[from] bunny_api_tokio::error::Error),
#[error(transparent)]
UrlParseError(#[from] url::ParseError),
#[error(transparent)]
PayloadError(#[from] PayloadError),
#[error("{0}")] #[error("{0}")]
PasswordHashError(String), PasswordHashError(String),
#[error("{0}")] #[error("{0}")]
@ -48,6 +54,7 @@ pub enum Error {
impl ResponseError for Error { impl ResponseError for Error {
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
debug!("{:?}", self);
error!("{}: {}", self.status_code(), self.to_string()); error!("{}: {}", self.status_code(), self.to_string());
HttpResponse::build(self.status_code()) HttpResponse::build(self.status_code())

View file

@ -32,9 +32,10 @@ struct Args {
pub struct Data { pub struct Data {
pub pool: deadpool::managed::Pool<AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>, Conn>, pub pool: deadpool::managed::Pool<AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>, Conn>,
pub cache_pool: redis::Client, pub cache_pool: redis::Client,
pub _config: Config, pub config: Config,
pub argon2: Argon2<'static>, pub argon2: Argon2<'static>,
pub start_time: SystemTime, pub start_time: SystemTime,
pub bunny_cdn: bunny_api_tokio::Client,
} }
#[tokio::main] #[tokio::main]
@ -57,6 +58,10 @@ async fn main() -> Result<(), Error> {
let cache_pool = redis::Client::open(config.cache_database.url())?; let cache_pool = redis::Client::open(config.cache_database.url())?;
let mut bunny_cdn = bunny_api_tokio::Client::new(config.bunny.api_key.clone()).await?;
bunny_cdn.storage.init(config.bunny.endpoint.clone(), config.bunny.storage_zone.clone())?;
let database_url = config.database.url(); let database_url = config.database.url();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
@ -90,10 +95,11 @@ async fn main() -> Result<(), Error> {
let data = Data { let data = Data {
pool, pool,
cache_pool, cache_pool,
_config: config, config,
// TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk)
argon2: Argon2::default(), argon2: Argon2::default(),
start_time: SystemTime::now(), start_time: SystemTime::now(),
bunny_cdn,
}; };
HttpServer::new(move || { HttpServer::new(move || {

View file

@ -48,6 +48,8 @@ diesel::table! {
name -> Varchar, name -> Varchar,
#[max_length = 300] #[max_length = 300]
description -> Nullable<Varchar>, description -> Nullable<Varchar>,
#[max_length = 100]
icon -> Nullable<Varchar>,
} }
} }
@ -121,6 +123,8 @@ diesel::table! {
email_verified -> Bool, email_verified -> Bool,
is_deleted -> Bool, is_deleted -> Bool,
deleted_at -> Nullable<Int8>, deleted_at -> Nullable<Int8>,
#[max_length = 100]
avatar -> Nullable<Varchar>,
} }
} }

View file

@ -1,9 +1,12 @@
use diesel::{delete, insert_into, prelude::{Insertable, Queryable}, ExpressionMethods, QueryDsl, Selectable, SelectableHelper}; use diesel::{delete, insert_into, prelude::{Insertable, Queryable}, update, ExpressionMethods, QueryDsl, Selectable, SelectableHelper};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use diesel_async::{pooled_connection::AsyncDieselConnectionManager, RunQueryDsl}; use diesel_async::{pooled_connection::AsyncDieselConnectionManager, RunQueryDsl};
use tokio::task;
use url::Url;
use actix_web::web::BytesMut;
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> { fn load_or_empty<T>(query_result: Result<Vec<T>, diesel::result::Error>) -> Result<Vec<T>, diesel::result::Error> {
match query_result { match query_result {
@ -238,6 +241,7 @@ struct GuildBuilder {
uuid: Uuid, uuid: Uuid,
name: String, name: String,
description: Option<String>, description: Option<String>,
icon: Option<String>,
owner_uuid: Uuid, owner_uuid: Uuid,
} }
@ -251,7 +255,7 @@ impl GuildBuilder {
uuid: self.uuid, uuid: self.uuid,
name: self.name, name: self.name,
description: self.description, description: self.description,
icon: String::from("bogus"), icon: self.icon.and_then(|i| i.parse().ok()),
owner_uuid: self.owner_uuid, owner_uuid: self.owner_uuid,
roles: roles, roles: roles,
member_count: member_count, member_count: member_count,
@ -264,7 +268,7 @@ pub struct Guild {
pub uuid: Uuid, pub uuid: Uuid,
name: String, name: String,
description: Option<String>, description: Option<String>,
icon: String, icon: Option<Url>,
owner_uuid: Uuid, owner_uuid: Uuid,
pub roles: Vec<Role>, pub roles: Vec<Role>,
member_count: i64, member_count: i64,
@ -323,6 +327,7 @@ impl Guild {
uuid: guild_uuid, uuid: guild_uuid,
name: name.clone(), name: name.clone(),
description: description.clone(), description: description.clone(),
icon: None,
owner_uuid, owner_uuid,
}; };
@ -349,7 +354,7 @@ impl Guild {
uuid: guild_uuid, uuid: guild_uuid,
name, name,
description, description,
icon: "bogus".to_string(), icon: None,
owner_uuid, owner_uuid,
roles: vec![], roles: vec![],
member_count: 1, member_count: 1,
@ -401,6 +406,37 @@ impl Guild {
Ok(invite) Ok(invite)
} }
// 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 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
.path()
.trim_start_matches('/');
bunny_cdn.storage.delete(relative_url).await?;
}
let path = format!("icons/{}/icon.{}", self.uuid, image_type);
bunny_cdn.storage.upload(path.clone(), icon.into()).await?;
let icon_url = cdn_url.join(&path)?;
use guilds::dsl;
update(guilds::table)
.filter(dsl::uuid.eq(self.uuid))
.set(dsl::icon.eq(icon_url.as_str()))
.execute(conn)
.await?;
self.icon = Some(icon_url);
Ok(())
}
} }
#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] #[derive(Serialize, Clone, Queryable, Selectable, Insertable)]
@ -571,6 +607,100 @@ impl Invite {
} }
} }
#[derive(Serialize, Clone, Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct User {
uuid: Uuid,
username: String,
display_name: Option<String>,
avatar: Option<String>,
}
impl User {
pub async fn fetch_one(conn: &mut Conn, user_uuid: Uuid) -> Result<Self, Error> {
use users::dsl;
let user: User = dsl::users
.filter(dsl::uuid.eq(user_uuid))
.select(User::as_select())
.get_result(conn)
.await?;
Ok(user)
}
pub async fn fetch_amount(conn: &mut Conn, offset: i64, amount: i64) -> Result<Vec<Self>, Error> {
use users::dsl;
let users: Vec<User> = load_or_empty(
dsl::users
.limit(amount)
.offset(offset)
.select(User::as_select())
.load(conn)
.await
)?;
Ok(users)
}
}
#[derive(Serialize, Queryable, Selectable)]
#[diesel(table_name = users)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Me {
uuid: Uuid,
username: String,
display_name: Option<String>,
avatar: Option<String>,
email: String,
email_verified: bool,
}
impl Me {
pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result<Self, Error> {
use users::dsl;
let me: Me = dsl::users
.filter(dsl::uuid.eq(user_uuid))
.select(Me::as_select())
.get_result(conn)
.await?;
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)] #[derive(Deserialize)]
pub struct StartAmountQuery { pub struct StartAmountQuery {
pub start: Option<i64>, pub start: Option<i64>,

View file

@ -1,7 +1,8 @@
use actix_web::{ use actix_web::{
cookie::{Cookie, SameSite, time::Duration}, cookie::{time::Duration, Cookie, SameSite},
http::header::HeaderMap, http::header::HeaderMap, web::BytesMut,
}; };
use bindet::FileType;
use getrandom::fill; use getrandom::fill;
use hex::encode; use hex::encode;
use redis::RedisError; use redis::RedisError;
@ -59,6 +60,22 @@ pub fn generate_refresh_token() -> Result<String, getrandom::Error> {
Ok(encode(buf)) 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 { impl Data {
pub async fn set_cache_key( pub async fn set_cache_key(
&self, &self,