diff --git a/spec/v2.json b/spec/v2.json index 887adb2..ec32bf4 100644 --- a/spec/v2.json +++ b/spec/v2.json @@ -44,7 +44,7 @@ { "name": "limit", "in": "query", - "description": "How many memes should be returned at maximum", + "description": "How many memes should be returned at maximum (-1 for no limit)", "required": false, "schema": { "type": "string" @@ -87,7 +87,7 @@ } }, "post": { - "summary": "Upload an image or video to JensMemes", + "summary": "Upload an image or video to JensMemes (WIP)", "security": [ { "discord": [] @@ -239,16 +239,36 @@ } } }, - "/memes/stream": { + "/memes/count": { "get": { - "summary": "Returns a stream of new uploaded memes", + "summary": "Gives the total number of memes", + "parameters": [ + { + "name": "category", + "in": "query", + "description": "Only count memes from this category ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "user", + "in": "query", + "description": "Only count memes from this user", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Stream of memes", + "description": "Amount of memes", "content": { - "application/x-json-stream": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Meme" + "$ref": "#/components/schemas/Count" } } } @@ -402,6 +422,103 @@ } } }, + "/users/{id}/memes" : { + "get": { + "summary": "Get all memes of a user", + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many memes should be returned at maximum (-1 for no limit)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "after", + "in": "query", + "description": "ID of the meme after which the returned memes should start", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Meme list response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Meme" + } + } + } + } + }, + "default": { + "description": "Some error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/users/{id}/memes/{filename}": { + "get": { + "summary": "Gives a specific meme from a user by filename", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The ID of the user", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "filename", + "in": "path", + "description": "The filename of the meme", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Meme response of this meme", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Meme" + } + } + } + }, + "default": { + "description": "Some error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, "/clips": { "get": { "summary": "WIP", @@ -475,12 +592,15 @@ "Meme": { "type": "object", "properties": { - "link": { + "filename": { "type": "string" }, "id": { "type": "integer" }, + "ipfs": { + "type": "string" + }, "category": { "type": "string" }, @@ -517,6 +637,14 @@ } } }, + "Count": { + "type": "object", + "properties": { + "count": { + "type": "integer" + } + } + }, "Clip": { "type": "object", "properties": { diff --git a/src/models.rs b/src/models.rs index aca533d..be9e4f4 100644 --- a/src/models.rs +++ b/src/models.rs @@ -26,6 +26,11 @@ pub struct User { pub dayuploads: i32, } +#[derive(Serialize)] +pub struct Count { + pub count: i64, +} + pub enum UserIdentifier { Id(String), Token(String), diff --git a/src/sql.rs b/src/sql.rs index 0b50af4..ab50a2d 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,5 +1,5 @@ use crate::ipfs::IPFSFile; -use crate::models::{Category, Meme, MemeOptions, User, UserIdentifier}; +use crate::models::{Category, Count, Meme, MemeOptions, User, UserIdentifier}; use crate::JMServiceInner; use sqlx::mysql::MySqlRow; use sqlx::{MySqlPool, Result, Row}; @@ -46,12 +46,13 @@ impl JMServiceInner { } pub async fn get_memes(&self, filter: MemeOptions) -> 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 ? AND memes.user LIKE ? AND memes.id > ?) ORDER BY memes.id") + 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 ? AND memes.user LIKE ? AND memes.id > ?) ORDER BY memes.id LIMIT ?") .bind(filter.category.unwrap_or_else(|| String::from("%"))) - .bind(format!("%{}%", filter.username.unwrap_or_else(String::new))) - .bind(format!("%{}%", filter.search.unwrap_or_else(String::new))) + .bind(format!("%{}%", filter.username.unwrap_or_default())) + .bind(format!("%{}%", filter.search.unwrap_or_default())) .bind(filter.user_id.unwrap_or_else(|| String::from("%"))) .bind(filter.after.unwrap_or(0)) + .bind(filter.limit.unwrap_or(100)) .map(|row: MySqlRow| Meme { id: row.get("id"), filename: row.get("filename"), @@ -68,8 +69,8 @@ impl JMServiceInner { pub async fn get_random_meme(&self, filter: MemeOptions) -> 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 ? AND memes.user LIKE ? AND memes.id > ?) ORDER BY RAND() LIMIT 1") .bind(filter.category.unwrap_or_else(|| String::from("%"))) - .bind(format!("%{}%", filter.username.unwrap_or_else(String::new))) - .bind(format!("%{}%", filter.search.unwrap_or_else(String::new))) + .bind(format!("%{}%", filter.username.unwrap_or_default())) + .bind(format!("%{}%", filter.search.unwrap_or_default())) .bind(filter.user_id.unwrap_or_else(|| String::from("%"))) .bind(filter.after.unwrap_or(0)) .map(|row: MySqlRow| Meme { @@ -85,6 +86,20 @@ impl JMServiceInner { Ok(q) } + pub async fn count_memes(&self, filter: MemeOptions) -> Result { + let q: Count = sqlx::query( + "SELECT COUNT(id) AS count FROM memes WHERE category LIKE ? AND user LIKE ?", + ) + .bind(filter.category.unwrap_or_else(|| String::from("%"))) + .bind(filter.user_id.unwrap_or_else(|| String::from("%"))) + .map(|row: MySqlRow| Count { + count: row.get("count"), + }) + .fetch_one(&self.db_pool) + .await?; + Ok(q) + } + pub async fn get_user_meme(&self, user_id: String, filename: String) -> 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.user = ? AND filename = ? ORDER BY memes.id DESC") .bind(user_id) diff --git a/src/v1/error.rs b/src/v1/error.rs index 3e83980..4fe3a90 100644 --- a/src/v1/error.rs +++ b/src/v1/error.rs @@ -41,7 +41,7 @@ impl IntoResponse for APIError { APIError::NotFound(err) => ErrorResponse::new(StatusCode::NOT_FOUND, Some(err)), APIError::Internal(err) => { ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err)) - } + }, APIError::Service(err) => ErrorResponse::new( StatusCode::INTERNAL_SERVER_ERROR, Some(err.get_response_message()), diff --git a/src/v1/models.rs b/src/v1/models.rs index 2bb85ab..3235a0c 100644 --- a/src/v1/models.rs +++ b/src/v1/models.rs @@ -139,7 +139,7 @@ impl From for MemeOptions { user_id: None, username: filter.user, search: filter.search, - limit: None, + limit: Some(-1), after: None, } } diff --git a/src/v1/routes.rs b/src/v1/routes.rs index 8da47b8..70db0bf 100644 --- a/src/v1/routes.rs +++ b/src/v1/routes.rs @@ -137,7 +137,7 @@ async fn upload( .to_string(); let file = service.ipfs_add(field.bytes().await?, filename).await?; files.push(file); - } + }, _ => (), } } diff --git a/src/v2/models.rs b/src/v2/models.rs index 018aeca..f8a05e2 100644 --- a/src/v2/models.rs +++ b/src/v2/models.rs @@ -1,5 +1,5 @@ -use crate::models::{Meme, User}; -use serde::Serialize; +use crate::models::{Meme, MemeOptions, User}; +use serde::{Deserialize, Serialize}; #[derive(Serialize)] pub struct V2Meme { @@ -18,6 +18,15 @@ pub struct V2User { pub dayuploads: i32, } +#[derive(Deserialize)] +pub struct MemeFilterQuery { + pub category: Option, + pub user: Option, + pub search: Option, + pub limit: Option, + pub after: Option, +} + #[derive(Serialize)] pub struct CDNEntry { pub directories: Vec, @@ -52,3 +61,16 @@ impl From for V2User { } } } + +impl From for MemeOptions { + fn from(query: MemeFilterQuery) -> Self { + Self { + category: query.category, + user_id: query.user, + username: None, + search: query.search, + limit: query.limit, + after: query.after, + } + } +} diff --git a/src/v2/routes.rs b/src/v2/routes.rs index ce60236..bd6e81a 100644 --- a/src/v2/routes.rs +++ b/src/v2/routes.rs @@ -12,7 +12,7 @@ use crate::{ JMService, }; -use super::models::{V2Meme, V2User}; +use super::models::{MemeFilterQuery, V2Meme, V2User}; async fn get_meme( Path(meme_id): Path, @@ -27,11 +27,12 @@ async fn get_meme( } async fn get_memes( + Query(filter): Query, Extension(service): Extension, ) -> Result { Ok(Json( service - .get_memes(MemeOptions::empty()) + .get_memes(filter.into()) .await? .into_iter() .map(V2Meme::from) @@ -39,6 +40,22 @@ async fn get_memes( )) } +async fn get_random_meme( + Query(filter): Query, + Extension(service): Extension, +) -> Result { + Ok(Json(V2Meme::from( + service.get_random_meme(filter.into()).await?, + ))) +} + +async fn count_memes( + Query(filter): Query, + Extension(service): Extension, +) -> Result { + Ok(Json(service.count_memes(filter.into()).await?)) +} + async fn get_category( Path(category_id): Path, Extension(service): Extension, @@ -80,6 +97,7 @@ async fn get_users( } async fn get_user_memes( + Query(filter): Query, Path(user_id): Path, Extension(service): Extension, ) -> Result { @@ -90,8 +108,8 @@ async fn get_user_memes( user_id: Some(user_id), username: None, search: None, - limit: None, - after: None, + limit: filter.limit, + after: filter.after, }) .await? .into_iter() @@ -113,15 +131,35 @@ async fn get_user_meme( ))) } -pub fn routes() -> Router { +fn meme_routes() -> Router { Router::new() - .route("/memes", get(get_memes)) - .route("/memes/:meme_id", get(get_meme)) - .route("/categories", get(get_categories)) - .route("/categories/:category_id", get(get_category)) - .route("/users", get(get_users)) - .route("/users/:user_id", get(get_user)) - .route("/users/:user_id/memes", get(get_user_memes)) - .route("/users/:user_id/memes/:filename", get(get_user_meme)) + .route("/", get(get_memes)) + .route("/:meme_id", get(get_meme)) + .route("/random", get(get_random_meme)) + .route("/count", get(count_memes)) + .boxed() +} + +fn category_routes() -> Router { + Router::new() + .route("/", get(get_categories)) + .route("/:category_id", get(get_category)) + .boxed() +} + +fn user_routes() -> Router { + Router::new() + .route("/", get(get_users)) + .route("/:user_id", get(get_user)) + .route("/:user_id/memes", get(get_user_memes)) + .route("/:user_id/memes/:filename", get(get_user_meme)) + .boxed() +} + +pub fn routes() -> Router { + Router::new() + .nest("/memes", meme_routes()) + .nest("/categories", category_routes()) + .nest("/users", user_routes()) .boxed() }