diff --git a/src/main.rs b/src/main.rs index ac1d573..90b8248 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,8 @@ mod config; mod error; mod ipfs; mod lib; +mod models; +mod sql; mod v1; #[derive(StructOpt)] diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..da63347 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize)] +pub struct Meme { + pub id: i32, + pub filename: String, + pub userid: String, + pub username: String, + pub category: String, + pub timestamp: i64, + pub ipfs: String, +} + +#[derive(Serialize)] +pub struct Category { + pub id: String, + pub name: String, +} + +#[derive(Serialize)] +pub struct User { + pub id: String, + pub name: String, + pub userdir: String, + pub tokenhash: String, + pub dayuploads: i32, +} + +pub enum UserIdentifier { + Id(String), + Token(String), + Username(String), + Null, +} + +#[derive(Deserialize)] +pub struct MemeFilter { + pub category: Option, + pub user: Option, + pub search: Option, +} diff --git a/src/sql.rs b/src/sql.rs new file mode 100644 index 0000000..f5a90fa --- /dev/null +++ b/src/sql.rs @@ -0,0 +1,145 @@ +use crate::ipfs::IPFSFile; +use crate::models::{Category, Meme, MemeFilter, User, UserIdentifier}; +use sqlx::mysql::MySqlRow; +use sqlx::{MySqlPool, Result, Row}; + +impl Meme { + pub async fn get(id: i32, pool: &MySqlPool) -> Result> { + let q: Option = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND memes.id=?").bind(id) + .map(|row: MySqlRow| Self { + id: row.get("id"), + filename: row.get("filename"), + username: row.get("name"), + userid: row.get("user"), + category: row.get("category"), + timestamp: row.get("ts"), + ipfs: row.get("cid"), + }) + .fetch_optional(pool).await?; + Ok(q) + } + + pub async fn get_all(filter: MemeFilter, pool: &MySqlPool) -> Result> { + let q: Vec = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY memes.id") + .bind(filter.category.unwrap_or_else(|| String::from("%"))) + .bind(format!("%{}%", filter.user.unwrap_or_else(String::new))) + .bind(format!("%{}%", filter.search.unwrap_or_else(String::new))) + .map(|row: MySqlRow| Self { + id: row.get("id"), + filename: row.get("filename"), + username: row.get("name"), + userid: row.get("user"), + category: row.get("category"), + timestamp: row.get("ts"), + ipfs: row.get("cid"), + }) + .fetch_all(pool).await?; + Ok(q) + } + + pub async fn get_random(filter: MemeFilter, pool: &MySqlPool) -> Result { + let q: Self = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY RAND() LIMIT 1") + .bind(filter.category.unwrap_or_else(|| String::from("%"))) + .bind(format!("%{}%", filter.user.unwrap_or_else(String::new))) + .bind(format!("%{}%", filter.search.unwrap_or_else(String::new))) + .map(|row: MySqlRow| Self { + id: row.get("id"), + filename: row.get("filename"), + username: row.get("name"), + userid: row.get("user"), + category: row.get("category"), + timestamp: row.get("ts"), + ipfs: row.get("cid"), + }) + .fetch_one(pool).await?; + Ok(q) + } +} + +impl Category { + pub async fn get(id: &String, pool: &MySqlPool) -> Result> { + let q: Option = sqlx::query("SELECT * FROM categories WHERE id=?") + .bind(id) + .map(|row: MySqlRow| Self { + id: row.get("id"), + name: row.get("name"), + }) + .fetch_optional(pool) + .await?; + Ok(q) + } + + pub async fn get_all(pool: &MySqlPool) -> Result> { + let q: Vec = sqlx::query("SELECT * FROM categories ORDER BY num") + .map(|row: MySqlRow| Self { + id: row.get("id"), + name: row.get("name"), + }) + .fetch_all(pool) + .await?; + Ok(q) + } + + pub async fn add_meme( + &self, + user: &User, + file: &IPFSFile, + ip: &String, + pool: &MySqlPool, + ) -> Result { + let mut tx = pool.begin().await?; + sqlx::query("INSERT INTO memes (filename, user, category, timestamp, ip, cid) VALUES (?, ?, ?, NOW(), ?, ?)") + .bind(&file.name) + .bind(&user.id) + .bind(&self.id) + .bind(ip) + .bind(&file.hash) + .execute(&mut tx).await?; + let id: u64 = sqlx::query("SELECT LAST_INSERT_ID() as id") + .map(|row: MySqlRow| row.get("id")) + .fetch_one(&mut tx) + .await?; + tx.commit().await?; + Ok(id) + } +} + +impl User { + pub async fn get(identifier: UserIdentifier, pool: &MySqlPool) -> Result> { + let query = match identifier { + UserIdentifier::Id(id) => sqlx::query("SELECT id, name, IFNULL(MD5(token), '0') AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users LEFT JOIN token ON users.id = token.uid WHERE users.id = ?").bind(id), + UserIdentifier::Token(token) => sqlx::query("SELECT id, name, IFNULL(MD5(token), '0') AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users LEFT JOIN token ON users.id = token.uid WHERE token = ?").bind(token), + UserIdentifier::Username(name) => sqlx::query("SELECT id, name, IFNULL(MD5(token), '0') AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users LEFT JOIN token ON users.id = token.uid WHERE name = ?").bind(name), + UserIdentifier::Null => sqlx::query("SELECT id, name, '0' AS hash, 0 AS uploads FROM users WHERE id = '000'"), + }; + let q: Option = query + .map(|row: MySqlRow| Self { + id: row.get("id"), + name: row.get("name"), + userdir: row.get("id"), + tokenhash: row.get("hash"), + dayuploads: row.get("uploads"), + }) + .fetch_optional(pool) + .await?; + Ok(q) + } + + pub async fn get_all(pool: &MySqlPool) -> Result> { + let q: Vec = sqlx::query("SELECT id, name, IFNULL(MD5(token), '0') AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users LEFT JOIN token ON users.id = token.uid") + .map(|row: MySqlRow| Self { + id: row.get("id"), + name: row.get("name"), + userdir: row.get("id"), + tokenhash: row.get("hash"), + dayuploads: row.get("uploads"), + }) + .fetch_all(pool).await?; + Ok(q) + } + + pub async fn check_token(token: &String, pool: &MySqlPool) -> Result> { + let user = Self::get(UserIdentifier::Token(token.clone()), pool).await?; + Ok(user) + } +} diff --git a/src/v1/error.rs b/src/v1/error.rs index c59fa3f..8a2dac8 100644 --- a/src/v1/error.rs +++ b/src/v1/error.rs @@ -25,6 +25,8 @@ pub enum APIError { #[error("{0}")] Forbidden(String), #[error("{0}")] + NotFound(String), + #[error("{0}")] Internal(String), #[error("IPFS error: {0}")] Ipfs(#[from] IPFSError), @@ -57,6 +59,7 @@ impl IntoResponse for APIError { APIError::BadRequest(err) => ErrorResponse::new(StatusCode::BAD_REQUEST, Some(err)), APIError::Unauthorized(err) => ErrorResponse::new(StatusCode::UNAUTHORIZED, Some(err)), APIError::Forbidden(err) => ErrorResponse::new(StatusCode::FORBIDDEN, Some(err)), + APIError::NotFound(err) => ErrorResponse::new(StatusCode::NOT_FOUND, Some(err)), APIError::Internal(err) => { ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err)) } diff --git a/src/v1/mod.rs b/src/v1/mod.rs index 40964c5..21c085f 100644 --- a/src/v1/mod.rs +++ b/src/v1/mod.rs @@ -1,7 +1,6 @@ mod error; pub mod models; mod routes; -mod sql; use async_trait::async_trait; use axum::extract::{FromRequest, RequestParts}; diff --git a/src/v1/models.rs b/src/v1/models.rs index ac598de..7291dba 100644 --- a/src/v1/models.rs +++ b/src/v1/models.rs @@ -1,6 +1,8 @@ use reqwest::StatusCode; use serde::{Deserialize, Serialize, Serializer}; +use crate::models::{Category, Meme, User, UserIdentifier}; + fn serialize_status(x: &StatusCode, s: S) -> Result where S: Serializer, @@ -9,7 +11,7 @@ where } #[derive(Serialize)] -pub struct Meme { +pub struct V1Meme { pub id: String, pub link: String, pub category: String, @@ -18,35 +20,20 @@ pub struct Meme { pub ipfs: String, } -#[derive(Serialize)] -pub struct Category { - pub id: String, - pub name: String, -} - -#[derive(Serialize)] -pub struct User { - pub id: String, - pub name: String, - pub userdir: String, - pub tokenhash: String, - pub dayuploads: i32, -} - //Responses #[derive(Serialize)] pub struct MemesResponse { pub status: i32, pub error: Option, - pub memes: Option>, + pub memes: Option>, } #[derive(Serialize)] pub struct MemeResponse { pub status: i32, pub error: Option, - pub meme: Option, + pub meme: Option, } #[derive(Serialize)] @@ -111,9 +98,29 @@ pub struct UserIDQuery { pub name: Option, } -#[derive(Deserialize)] -pub struct MemeFilterQuery { - pub category: Option, - pub user: Option, - pub search: Option, +impl V1Meme { + pub fn new(meme: Meme, cdn: String) -> Self { + Self { + id: meme.id.to_string(), + link: format!("{}/{}/{}", cdn, meme.userid, meme.filename), + category: meme.category, + user: meme.username, + timestamp: meme.timestamp.to_string(), + ipfs: meme.ipfs, + } + } +} + +impl From for UserIdentifier { + fn from(query: UserIDQuery) -> Self { + if let Some(id) = query.id { + Self::Id(id) + } else if let Some(token) = query.token { + Self::Token(token) + } else if let Some(name) = query.name { + Self::Username(name) + } else { + Self::Null + } + } } diff --git a/src/v1/routes.rs b/src/v1/routes.rs index 3bda2da..632af7e 100644 --- a/src/v1/routes.rs +++ b/src/v1/routes.rs @@ -1,6 +1,7 @@ use crate::config::ConfVars; use crate::ipfs::IPFSFile; use crate::lib::ExtractIP; +use crate::models::{Category, Meme, MemeFilter, User}; use crate::v1::models::*; use axum::extract::{ContentLengthLimit, Extension, Multipart}; @@ -19,7 +20,12 @@ async fn meme( Extension(db_pool): Extension, Extension(vars): Extension, ) -> Result { - let meme = Meme::get(params.id, &db_pool, vars.cdn).await?; + let meme = V1Meme::new( + Meme::get(params.id, &db_pool) + .await? + .ok_or_else(|| APIError::NotFound("Meme not found".to_string()))?, + vars.cdn, + ); Ok(Json(MemeResponse { status: 200, error: None, @@ -28,11 +34,15 @@ async fn meme( } async fn memes( - Query(params): Query, + Query(params): Query, Extension(db_pool): Extension, Extension(vars): Extension, ) -> Result { - let memes = Meme::get_all(params, &db_pool, vars.cdn).await?; + let memes = Meme::get_all(params, &db_pool) + .await? + .into_iter() + .map(|meme| V1Meme::new(meme, vars.cdn.clone())) + .collect(); Ok(Json(MemesResponse { status: 200, error: None, @@ -44,7 +54,9 @@ async fn category( Query(params): Query, Extension(db_pool): Extension, ) -> Result { - let category = Category::get(¶ms.id, &db_pool).await?; + let category = Category::get(¶ms.id, &db_pool) + .await? + .ok_or_else(|| APIError::NotFound("Category not found".to_string()))?; Ok(Json(CategoryResponse { status: 200, error: None, @@ -67,7 +79,9 @@ async fn user( Query(params): Query, Extension(db_pool): Extension, ) -> Result { - let user = User::get(params, &db_pool).await?; + let user = User::get(params.into(), &db_pool) + .await? + .ok_or_else(|| APIError::NotFound("User not found".to_string()))?; Ok(Json(UserResponse { status: 200, error: None, @@ -85,11 +99,11 @@ async fn users(Extension(db_pool): Extension) -> Result, + Query(params): Query, Extension(db_pool): Extension, Extension(vars): Extension, ) -> Result { - let random = Meme::get_random(params, &db_pool, vars.cdn).await?; + let random = V1Meme::new(Meme::get_random(params, &db_pool).await?, vars.cdn); Ok(Json(MemeResponse { status: 200, error: None, @@ -140,7 +154,9 @@ async fn upload( return Err(APIError::Forbidden("Upload limit reached".to_string())); } - let cat = Category::get(&category, &db_pool).await?; + let cat = Category::get(&category, &db_pool) + .await? + .ok_or_else(|| APIError::BadRequest("Category not existing".to_string()))?; let ip = ip.to_string(); diff --git a/src/v1/sql.rs b/src/v1/sql.rs deleted file mode 100644 index 7a4ba98..0000000 --- a/src/v1/sql.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::ipfs::IPFSFile; -use crate::v1::models::{Category, Meme, MemeFilterQuery, User, UserIDQuery}; -use sqlx::mysql::MySqlRow; -use sqlx::{MySqlPool, Result, Row}; - -pub struct DBMeme { - pub id: i32, - pub filename: String, - pub user: String, - pub userdir: String, - pub category: String, - pub timestamp: i64, - pub ipfs: String, -} - -impl Meme { - pub fn new(meme: DBMeme, cdn: String) -> Self { - Self { - id: meme.id.to_string(), - link: format!("{}/{}/{}", cdn, meme.userdir, meme.filename), - category: meme.category, - user: meme.user, - timestamp: meme.timestamp.to_string(), - ipfs: meme.ipfs, - } - } - - pub async fn get(id: i32, pool: &MySqlPool, cdn: String) -> Result { - let q: Self = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND memes.id=?").bind(id) - .map(|row: MySqlRow| Self::new(DBMeme { - id: row.get("id"), - filename: row.get("filename"), - user: row.get("name"), - userdir: row.get("user"), - category: row.get("category"), - timestamp: row.get("ts"), - ipfs: row.get("cid"), - }, cdn.clone())) - .fetch_one(pool).await?; - Ok(q) - } - - pub async fn get_all( - params: MemeFilterQuery, - pool: &MySqlPool, - cdn: String, - ) -> Result> { - let q: Vec = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY memes.id") - .bind(params.category.unwrap_or_else(|| String::from("%"))) - .bind(format!("%{}%", params.user.unwrap_or_else(String::new))) - .bind(format!("%{}%", params.search.unwrap_or_else(String::new))) - .map(|row: MySqlRow| Self::new(DBMeme { - id: row.get("id"), - filename: row.get("filename"), - user: row.get("name"), - userdir: row.get("user"), - category: row.get("category"), - timestamp: row.get("ts"), - ipfs: row.get("cid"), - }, cdn.clone())) - .fetch_all(pool).await?; - Ok(q) - } - - pub async fn get_random( - params: MemeFilterQuery, - pool: &MySqlPool, - cdn: String, - ) -> Result { - let q: Meme = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts, cid FROM memes, users WHERE memes.user = users.id AND (category LIKE ? AND name LIKE ? AND filename LIKE ?) ORDER BY RAND() LIMIT 1") - .bind(params.category.unwrap_or_else(|| String::from("%"))) - .bind(format!("%{}%", params.user.unwrap_or_else(String::new))) - .bind(format!("%{}%", params.search.unwrap_or_else(String::new))) - .map(|row: MySqlRow| Self::new(DBMeme { - id: row.get("id"), - filename: row.get("filename"), - user: row.get("name"), - userdir: row.get("user"), - category: row.get("category"), - timestamp: row.get("ts"), - ipfs: row.get("cid"), - }, cdn.clone())) - .fetch_one(pool).await?; - Ok(q) - } -} - -impl Category { - pub async fn get(id: &String, pool: &MySqlPool) -> Result { - let q: Category = sqlx::query("SELECT * FROM categories WHERE id=?") - .bind(id) - .map(|row: MySqlRow| Self { - id: row.get("id"), - name: row.get("name"), - }) - .fetch_one(pool) - .await?; - Ok(q) - } - - pub async fn get_all(pool: &MySqlPool) -> Result> { - let q: Vec = sqlx::query("SELECT * FROM categories ORDER BY num") - .map(|row: MySqlRow| Self { - id: row.get("id"), - name: row.get("name"), - }) - .fetch_all(pool) - .await?; - Ok(q) - } - - pub async fn add_meme( - &self, - user: &User, - file: &IPFSFile, - ip: &String, - pool: &MySqlPool, - ) -> Result { - let mut tx = pool.begin().await?; - sqlx::query("INSERT INTO memes (filename, user, category, timestamp, ip, cid) VALUES (?, ?, ?, NOW(), ?, ?)") - .bind(&file.name) - .bind(&user.id) - .bind(&self.id) - .bind(ip) - .bind(&file.hash) - .execute(&mut tx).await?; - let id: u64 = sqlx::query("SELECT LAST_INSERT_ID() as id") - .map(|row: MySqlRow| row.get("id")) - .fetch_one(&mut tx) - .await?; - tx.commit().await?; - Ok(id) - } -} - -impl User { - pub async fn get(params: UserIDQuery, pool: &MySqlPool) -> Result { - let q: User = sqlx::query("SELECT id, name, MD5(token) AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users, token WHERE users.id = token.uid AND (users.id LIKE ? OR token LIKE ? OR name LIKE ?) UNION SELECT id, name, 0 AS hash, 0 AS uploads FROM users WHERE id = '000'") - .bind(params.id.unwrap_or_else(String::new)) - .bind(params.token.unwrap_or_else(String::new)) - .bind(params.name.unwrap_or_else(String::new)) - .map(|row: MySqlRow| Self { - id: row.get("id"), - name: row.get("name"), - userdir: row.get("id"), - tokenhash: row.get("hash"), - dayuploads: row.get("uploads"), - }) - .fetch_one(pool).await?; - Ok(q) - } - - pub async fn get_all(pool: &MySqlPool) -> Result> { - let q: Vec = sqlx::query("SELECT id, name, MD5(token) AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users, token WHERE users.id = token.uid UNION SELECT id, name, 0 AS hash, 0 AS uploads FROM users WHERE id = '000'") - .map(|row: MySqlRow| Self { - id: row.get("id"), - name: row.get("name"), - userdir: row.get("id"), - tokenhash: row.get("hash"), - dayuploads: row.get("uploads"), - }) - .fetch_all(pool).await?; - Ok(q) - } - - pub async fn check_token(token: &String, pool: &MySqlPool) -> Result> { - let q: Option = sqlx::query("SELECT id, name, MD5(token) AS hash, uploads FROM (SELECT id, name, IFNULL(count.uploads, 0) AS uploads FROM users LEFT JOIN (SELECT user, COUNT(*) AS uploads FROM memes WHERE DATE(timestamp) = CURDATE() GROUP BY (user)) AS count ON users.id = count.user) AS users, token WHERE users.id = token.uid AND token = ?") - .bind(token) - .map(|row: MySqlRow| Self { - id: row.get("id"), - name: row.get("name"), - userdir: row.get("id"), - tokenhash: row.get("hash"), - dayuploads: row.get("uploads"), - }) - .fetch_optional(pool).await?; - Ok(q) - } -}