This commit is contained in:
Timo Ley 2021-07-19 22:29:03 +02:00
commit 039f785129
8 changed files with 3068 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
.idea
*.iml

2691
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "jmserver"
version = "0.1.0"
authors = ["Timo Ley <timo@leyt.xyz>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.51"
sqlx = { version = "0.3", features = [ "mysql" ] }
rand = "0.8.0"

21
src/main.rs Normal file
View file

@ -0,0 +1,21 @@
use actix_web::{HttpServer, App};
use std::{io, env};
use sqlx::MySqlPool;
mod v1;
#[actix_web::main]
async fn main() -> io::Result<()>{
let database_url = env::var("DBURL").unwrap();
let db_pool = MySqlPool::new(&database_url).await.unwrap();
let mut server = HttpServer::new(move || {
App::new()
.data(db_pool.clone())
.configure(v1::init)
});
server = server.bind(env::var("LISTEN").unwrap())?;
server.run().await
}

5
src/v1/mod.rs Normal file
View file

@ -0,0 +1,5 @@
mod routes;
pub mod models;
mod sql;
pub use routes::init;

104
src/v1/models.rs Normal file
View file

@ -0,0 +1,104 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct Meme {
pub id: String,
pub link: String,
pub category: String,
pub user: String,
pub timestamp: 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<String>,
pub memes: Option<Vec<Meme>>,
}
#[derive(Serialize)]
pub struct MemeResponse {
pub status: i32,
pub error: Option<String>,
pub meme: Option<Meme>,
}
#[derive(Serialize)]
pub struct CategoriesResponse {
pub status: i32,
pub error: Option<String>,
pub categories: Option<Vec<Category>>,
}
#[derive(Serialize)]
pub struct CategoryResponse {
pub status: i32,
pub error: Option<String>,
pub category: Option<Category>,
}
#[derive(Serialize)]
pub struct UsersResponse {
pub status: i32,
pub error: Option<String>,
pub users: Option<Vec<User>>,
}
#[derive(Serialize)]
pub struct UserResponse {
pub status: i32,
pub error: Option<String>,
pub user: Option<User>,
}
#[derive(Serialize)]
pub struct UploadResponse {
pub status: i32,
pub error: Option<String>,
pub files: Option<Vec<String>>
}
//Query
#[derive(Deserialize)]
pub struct IDQuery {
pub id: String,
}
#[derive(Deserialize)]
pub struct MemeIDQuery {
pub id: i32,
}
#[derive(Deserialize)]
pub struct UserIDQuery {
pub id: Option<String>,
pub token: Option<String>,
pub name: Option<String>,
}
#[derive(Deserialize)]
pub struct MemeFilterQuery {
pub category: Option<String>,
pub user: Option<String>,
pub search: Option<String>,
}

115
src/v1/routes.rs Normal file
View file

@ -0,0 +1,115 @@
use actix_web::{web, get, Responder, HttpResponse};
use crate::v1::models::*;
use sqlx::{MySqlPool, Error};
#[get("/v1/meme")]
async fn meme(params: web::Query<MemeIDQuery>, db_pool: web::Data<MySqlPool>) -> impl Responder {
let q = Meme::get(params.id, db_pool.get_ref()).await;
match q {
Ok(meme) => HttpResponse::Ok().json(MemeResponse {
status: 200,
error: None,
meme: Option::from(meme)
}),
Err(err) => match err {
Error::RowNotFound => HttpResponse::NotFound().json(MemeResponse {
status: 404,
error: Option::from(String::from("Meme not found")),
meme: None
}),
_ => HttpResponse::InternalServerError().json(MemeResponse {
status: 500,
error: Option::from(String::from("Internal Server Error")),
meme: None
})
}
}
}
#[get("/v1/memes")]
async fn memes(params: web::Query<MemeFilterQuery>, db_pool: web::Data<MySqlPool>) -> impl Responder {
let q = Meme::get_all(params.0, db_pool.get_ref()).await;
match q {
Ok(memes) => HttpResponse::Ok().json(MemesResponse {
status: 200,
error: None,
memes: Option::from(memes)
}),
_ => HttpResponse::InternalServerError().json(MemesResponse {
status: 500,
error: Option::from(String::from("Internal Server Error")),
memes: None
})
}
}
#[get("/v1/category")]
async fn category(params: web::Query<IDQuery>, db_pool: web::Data<MySqlPool>) -> impl Responder {
let q = Category::get(&params.id, db_pool.get_ref()).await;
match q {
Ok(category) => HttpResponse::Ok().json(CategoryResponse { status: 200, error: None, category: Option::from(category)}),
Err(err) => match err {
Error::RowNotFound => HttpResponse::NotFound().json(CategoryResponse {
status: 404,
error: Option::from(String::from("Category not found")),
category: None
}),
_ => HttpResponse::InternalServerError().json(CategoryResponse {
status: 500,
error: Option::from(String::from("Internal Server Error")),
category: None
})
}
}
}
#[get("/v1/categories")]
async fn categories(db_pool: web::Data<MySqlPool>) -> impl Responder {
let q = Category::get_all(db_pool.get_ref()).await;
match q {
Ok(categories) => HttpResponse::Ok().json(CategoriesResponse { status: 200, error: None, categories: Option::from(categories)}),
_ => HttpResponse::InternalServerError().json(CategoriesResponse {
status: 500,
error: Option::from(String::from("Internal Server Error")),
categories: None
})
}
}
#[get("/v1/user")]
async fn user(params: web::Query<UserIDQuery>, db_pool: web::Data<MySqlPool>) -> impl Responder {
let q = User::get(params.0,db_pool.get_ref()).await;
match q {
Ok(user) => HttpResponse::Ok().json(UserResponse { status: 200, error: None, user: Option::from(user)}),
_ => HttpResponse::InternalServerError().json(UserResponse {
status: 500,
error: Option::from(String::from("Internal Server Error")),
user: None
})
}
}
#[get("/v1/users")]
async fn users(db_pool: web::Data<MySqlPool>) -> impl Responder {
let q = User::get_all(db_pool.get_ref()).await;
match q {
Ok(users) => HttpResponse::Ok().json(UsersResponse { status: 200, error: None, users: Option::from(users)}),
_ => HttpResponse::InternalServerError().json(UsersResponse {
status: 500,
error: Option::from(String::from("Internal Server Error")),
users: None
})
}
}
//TODO: Implement random meme endpoint
//TODO: Implement upload endpoint
pub fn init(cfg: &mut web::ServiceConfig) {
cfg.service(meme);
cfg.service(memes);
cfg.service(category);
cfg.service(categories);
cfg.service(user);
cfg.service(users);
}

115
src/v1/sql.rs Normal file
View file

@ -0,0 +1,115 @@
use crate::v1::models::{Meme, MemeFilterQuery, Category, User, UserIDQuery};
use sqlx::{MySqlPool, Result, Row};
use sqlx::mysql::MySqlRow;
use std::env;
pub struct DBMeme {
pub id: i32,
pub filename: String,
pub user: String,
pub userdir: String,
pub category: String,
pub timestamp: i64,
}
impl Meme {
pub async fn get(id: i32, pool: &MySqlPool) -> Result<Meme> {
let q: Meme = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts FROM memes, users WHERE memes.user = users.id AND memes.id=?").bind(id)
.map(|row: MySqlRow| Meme::from(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"),
}))
.fetch_one(pool).await?;
Ok(q)
}
pub async fn get_all(params: MemeFilterQuery, pool: &MySqlPool) -> Result<Vec<Meme>> {
let q: Vec<Meme> = sqlx::query("SELECT memes.id, user, filename, category, name, UNIX_TIMESTAMP(timestamp) AS ts 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(String::from("%")))
.bind(format!("%{}%", params.user.unwrap_or(String::from(""))))
.bind(format!("%{}%", params.search.unwrap_or(String::from(""))))
.map(|row: MySqlRow| Meme::from(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"),
}))
.fetch_all(pool).await?;
Ok(q)
}
}
impl From<DBMeme> for Meme {
fn from(meme: DBMeme) -> Self {
Meme {
id: meme.id.to_string(),
link: format!("{}/{}/{}", env::var("CDNURL").unwrap(), meme.userdir, meme.filename),
category: meme.category,
user: meme.user,
timestamp: meme.timestamp.to_string(),
}
}
}
impl Category {
pub async fn get(id: &String, pool: &MySqlPool) -> Result<Category> {
let q: Category = sqlx::query("SELECT * FROM categories WHERE id=?").bind(id)
.map(|row: MySqlRow| Category {
id: row.get("id"),
name: row.get("name"),
})
.fetch_one(pool).await?;
Ok(q)
}
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Category>> {
let q: Vec<Category> = sqlx::query("SELECT * FROM categories ORDER BY num")
.map(|row: MySqlRow| Category {
id: row.get("id"),
name: row.get("name"),
})
.fetch_all(pool).await?;
Ok(q)
}
}
impl User {
pub async fn get(params: UserIDQuery, pool: &MySqlPool) -> Result<User> {
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(String::from("")))
.bind(params.token.unwrap_or(String::from("")))
.bind(params.name.unwrap_or(String::from("")))
.map(|row: MySqlRow| User {
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<Vec<User>> {
let q: Vec<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 UNION SELECT id, name, 0 AS hash, 0 AS uploads FROM users WHERE id = '000'")
.map(|row: MySqlRow| User {
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)
}
}