started v3.0.0 development
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
a50d157394
commit
1577b9a1ca
17 changed files with 1025 additions and 162 deletions
|
@ -1,3 +1,3 @@
|
|||
- Initial Matrix implementation
|
||||
- Allows bots to know, when a new meme was uploaded
|
||||
- First step towards decentralization
|
||||
- Refactoring
|
||||
- Initial V2 API
|
||||
- Added Dockerfilegit
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
|
@ -0,0 +1,21 @@
|
|||
FROM rust:buster as builder
|
||||
|
||||
RUN apt update && apt install -y libssl-dev
|
||||
|
||||
WORKDIR /usr/src/jmserver
|
||||
|
||||
COPY Cargo.toml ./
|
||||
COPY src/ src/
|
||||
COPY templates/ templates/
|
||||
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:buster
|
||||
|
||||
COPY --from=builder /usr/src/jmserver/target/release/jmserver /usr/bin
|
||||
|
||||
RUN apt update && apt install -y libssl1.1 dumb-init
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/bin/jmserver", "--config", "/data/config.toml"]
|
573
spec/v2.json
Normal file
573
spec/v2.json
Normal file
|
@ -0,0 +1,573 @@
|
|||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"version": "2.0.0",
|
||||
"title": "JensMemes"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.tilera.xyz/jensmemes/v2"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/memes": {
|
||||
"get": {
|
||||
"summary": "List all memes on JensMemes",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "category",
|
||||
"in": "query",
|
||||
"description": "Filter category of the memes",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"in": "query",
|
||||
"description": "Filter user of the memes",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"in": "query",
|
||||
"description": "Search for memes",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "How many memes should be returned at maximum",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Upload an image or video to JensMemes",
|
||||
"security": [
|
||||
{
|
||||
"discord": []
|
||||
},
|
||||
{
|
||||
"token": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "The ID of the category of the meme"
|
||||
},
|
||||
"file": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "The file or files to upload to JensMemes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Response of the upload",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Meme"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Some error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/memes/{id}": {
|
||||
"get": {
|
||||
"summary": "Gives a specific meme by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "The ID of the meme",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/memes/random": {
|
||||
"get": {
|
||||
"summary": "Gives a random meme",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "category",
|
||||
"in": "query",
|
||||
"description": "Only give a random meme from this category ID",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"in": "query",
|
||||
"description": "Only give a random meme from this user",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Meme response of a random meme",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meme"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Some error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/memes/stream": {
|
||||
"get": {
|
||||
"summary": "Returns a stream of new uploaded memes",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Stream of memes",
|
||||
"content": {
|
||||
"application/x-json-stream": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Meme"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Some error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/categories": {
|
||||
"get": {
|
||||
"summary": "Get all categories available on JensMemes",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of all categories on JensMemes",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Category"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Some error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/categories/{id}": {
|
||||
"get": {
|
||||
"summary": "Get a specific category by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "ID of the category",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The requested category",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Category"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Some error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "Get all users registered on JensMemes",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "All users on JensMemes",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Some error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}": {
|
||||
"get": {
|
||||
"summary": "Get a specific user on JensMemes",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "The ID of the user",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The requested user",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Some error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/clips": {
|
||||
"get": {
|
||||
"summary": "WIP",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "streamer",
|
||||
"in": "query",
|
||||
"description": "Twitch username of the streamer",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Clip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "WIP",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"link": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"discord": []
|
||||
},
|
||||
{
|
||||
"token": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Uploaded",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Clip"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Meme": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Category": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"dayuploads": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Clip": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"streamer": {
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ErrorResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status",
|
||||
"error"
|
||||
],
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "integer",
|
||||
"minimum": 200,
|
||||
"maximum": 500
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"discord": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "Authorization",
|
||||
"description": "A Discord OAuth Token, prefix with 'Discord '"
|
||||
},
|
||||
"token": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "Authorization",
|
||||
"description": "A JWT Token from the bot, prefix with 'Token '"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ use reqwest::{
|
|||
header::{HeaderName, CONTENT_LENGTH},
|
||||
StatusCode,
|
||||
};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use crate::JMService;
|
||||
|
||||
|
@ -35,12 +34,11 @@ pub fn routes() -> Router<BoxRoute> {
|
|||
|
||||
async fn image(
|
||||
Path((user, filename)): Path<(String, String)>,
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, CDNError> {
|
||||
let filename = urlencoding::decode(&filename)?.into_owned();
|
||||
let cid = sql::get_cid(user, filename.clone(), &db_pool).await?;
|
||||
let res = service.cat(cid).await?;
|
||||
let cid = sql::get_cid(user, filename.clone(), &service.db_pool).await?;
|
||||
let res = service.ipfs_cat(cid).await?;
|
||||
let clength = res
|
||||
.headers()
|
||||
.get(HeaderName::from_static("x-content-length"))
|
||||
|
@ -58,11 +56,8 @@ async fn image(
|
|||
))
|
||||
}
|
||||
|
||||
async fn users(
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, CDNError> {
|
||||
let users = sql::get_users(&db_pool).await?;
|
||||
async fn users(Extension(service): Extension<JMService>) -> Result<impl IntoResponse, CDNError> {
|
||||
let users = sql::get_users(&service.db_pool).await?;
|
||||
Ok(HtmlTemplate(DirTemplate {
|
||||
entries: users,
|
||||
prefix: service.cdn_url(),
|
||||
|
@ -72,9 +67,9 @@ async fn users(
|
|||
|
||||
async fn memes(
|
||||
Path(user): Path<String>,
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, CDNError> {
|
||||
let memes = sql::get_memes(user, &db_pool).await?;
|
||||
let memes = sql::get_memes(user, &service.db_pool).await?;
|
||||
Ok(HtmlTemplate(DirTemplate {
|
||||
entries: memes,
|
||||
prefix: ".".to_string(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use reqwest::Url;
|
||||
use serde::Deserialize;
|
||||
use sqlx::MySqlPool;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use crate::{error::JMError, JMService, JMServiceInner};
|
||||
|
@ -16,10 +17,11 @@ pub struct Config {
|
|||
}
|
||||
|
||||
impl Config {
|
||||
pub fn service(&self) -> Result<JMService, JMError> {
|
||||
pub fn service(&self, db_pool: MySqlPool) -> Result<JMService, JMError> {
|
||||
let client = reqwest::ClientBuilder::new().user_agent("curl").build()?;
|
||||
Ok(Arc::new(JMServiceInner {
|
||||
client,
|
||||
db_pool,
|
||||
ipfs_url: self.ipfs_api.clone(),
|
||||
cdn_url: self.cdn.clone(),
|
||||
matrix_url: self.matrix_url.clone(),
|
||||
|
|
27
src/error.rs
27
src/error.rs
|
@ -1,3 +1,6 @@
|
|||
use std::string::FromUtf8Error;
|
||||
|
||||
use axum::extract::{multipart::MultipartError, rejection::QueryRejection};
|
||||
use hyper::StatusCode;
|
||||
use thiserror::Error;
|
||||
use url::ParseError;
|
||||
|
@ -25,3 +28,27 @@ pub enum ServiceError {
|
|||
#[error("Invalid response code: {0}")]
|
||||
InvalidResponse(StatusCode),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum APIError {
|
||||
#[error("SQL error: {0}")]
|
||||
Sql(#[from] sqlx::Error),
|
||||
#[error("Multipart form error: {0}")]
|
||||
Multipart(#[from] MultipartError),
|
||||
#[error("{0}")]
|
||||
BadRequest(String),
|
||||
#[error("{0}")]
|
||||
Unauthorized(String),
|
||||
#[error("{0}")]
|
||||
Forbidden(String),
|
||||
#[error("{0}")]
|
||||
NotFound(String),
|
||||
#[error("{0}")]
|
||||
Internal(String),
|
||||
#[error("JMService error: {0}")]
|
||||
Service(#[from] ServiceError),
|
||||
#[error("Query rejection: {0}")]
|
||||
Query(#[from] QueryRejection),
|
||||
#[error("Decode error: {0}")]
|
||||
Decode(#[from] FromUtf8Error),
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ pub struct PinQuery {
|
|||
}
|
||||
|
||||
impl JMServiceInner {
|
||||
pub async fn cat(&self, cid: String) -> Result<Response, ServiceError> {
|
||||
pub async fn ipfs_cat(&self, cid: String) -> Result<Response, ServiceError> {
|
||||
let request = self
|
||||
.client
|
||||
.post(self.ipfs_url.join("/api/v0/cat")?)
|
||||
|
@ -43,7 +43,7 @@ impl JMServiceInner {
|
|||
Ok(request.send().await?)
|
||||
}
|
||||
|
||||
pub async fn add(&self, file: Bytes, filename: String) -> Result<IPFSFile, ServiceError> {
|
||||
pub async fn ipfs_add(&self, file: Bytes, filename: String) -> Result<IPFSFile, ServiceError> {
|
||||
let request = self
|
||||
.client
|
||||
.post(self.ipfs_url.join("/api/v0/add")?)
|
||||
|
@ -54,7 +54,7 @@ impl JMServiceInner {
|
|||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn pin(&self, cid: String) -> Result<(), ServiceError> {
|
||||
pub async fn ipfs_pin(&self, cid: String) -> Result<(), ServiceError> {
|
||||
let request = self
|
||||
.client
|
||||
.post(self.ipfs_url.join("/api/v0/pin/add")?)
|
||||
|
|
|
@ -20,6 +20,7 @@ mod matrix;
|
|||
mod models;
|
||||
mod sql;
|
||||
mod v1;
|
||||
mod v2;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
struct Opt {
|
||||
|
@ -34,6 +35,7 @@ struct Opt {
|
|||
|
||||
pub struct JMServiceInner {
|
||||
client: Client,
|
||||
db_pool: MySqlPool,
|
||||
ipfs_url: Url,
|
||||
cdn_url: String,
|
||||
matrix_url: Url,
|
||||
|
@ -50,12 +52,12 @@ async fn main() -> Result<(), JMError> {
|
|||
let config = toml::from_slice::<Config>(&config)?;
|
||||
|
||||
let db_pool = MySqlPool::new(&config.database).await?;
|
||||
let service = config.service()?;
|
||||
let service = config.service(db_pool)?;
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/api/v1", v1::routes())
|
||||
.nest("/api/v2", v2::routes())
|
||||
.nest("/cdn", cdn::routes())
|
||||
.layer(AddExtensionLayer::new(db_pool))
|
||||
.layer(AddExtensionLayer::new(service))
|
||||
.layer(SetResponseHeaderLayer::<_, Request<Body>>::if_not_present(
|
||||
header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Meme {
|
||||
|
@ -33,9 +33,24 @@ pub enum UserIdentifier {
|
|||
Null,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MemeFilter {
|
||||
pub struct MemeOptions {
|
||||
pub category: Option<String>,
|
||||
pub user: Option<String>,
|
||||
pub user_id: Option<String>,
|
||||
pub username: Option<String>,
|
||||
pub search: Option<String>,
|
||||
pub limit: Option<i32>,
|
||||
pub after: Option<i32>,
|
||||
}
|
||||
|
||||
impl MemeOptions {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
category: None,
|
||||
user_id: None,
|
||||
username: None,
|
||||
search: None,
|
||||
limit: None,
|
||||
after: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
219
src/sql.rs
219
src/sql.rs
|
@ -1,85 +1,10 @@
|
|||
use crate::ipfs::IPFSFile;
|
||||
use crate::models::{Category, Meme, MemeFilter, User, UserIdentifier};
|
||||
use crate::models::{Category, Meme, MemeOptions, User, UserIdentifier};
|
||||
use crate::JMServiceInner;
|
||||
use sqlx::mysql::MySqlRow;
|
||||
use sqlx::{MySqlPool, Result, Row};
|
||||
|
||||
impl Meme {
|
||||
pub async fn get(id: i32, pool: &MySqlPool) -> Result<Option<Self>> {
|
||||
let q: Option<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 {
|
||||
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<Vec<Self>> {
|
||||
let q: Vec<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 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<Self> {
|
||||
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<Option<Self>> {
|
||||
let q: Option<Self> = 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<Vec<Self>> {
|
||||
let q: Vec<Self> = 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,
|
||||
|
@ -104,42 +29,160 @@ impl Category {
|
|||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn get(identifier: UserIdentifier, pool: &MySqlPool) -> Result<Option<Self>> {
|
||||
impl JMServiceInner {
|
||||
pub async fn get_meme(&self, id: i32) -> Result<Option<Meme>> {
|
||||
let q: Option<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 memes.id=?").bind(id)
|
||||
.map(|row: MySqlRow| Meme {
|
||||
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(&self.db_pool).await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn get_memes(&self, filter: MemeOptions) -> Result<Vec<Meme>> {
|
||||
let q: Vec<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 memes.id")
|
||||
.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(filter.user_id.unwrap_or_else(|| String::from("%")))
|
||||
.bind(filter.after.unwrap_or(0))
|
||||
.map(|row: MySqlRow| Meme {
|
||||
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(&self.db_pool).await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn get_random_meme(&self, filter: MemeOptions) -> Result<Meme> {
|
||||
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(filter.user_id.unwrap_or_else(|| String::from("%")))
|
||||
.bind(filter.after.unwrap_or(0))
|
||||
.map(|row: MySqlRow| Meme {
|
||||
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(&self.db_pool).await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn get_user_meme(&self, user_id: String, filename: String) -> Result<Option<Meme>> {
|
||||
let q: Option<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 memes.user = ? AND filename = ? ORDER BY memes.id DESC")
|
||||
.bind(user_id)
|
||||
.bind(filename)
|
||||
.map(|row: MySqlRow| Meme {
|
||||
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(&self.db_pool).await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn get_category(&self, id: &String) -> Result<Option<Category>> {
|
||||
let q: Option<Category> = sqlx::query("SELECT * FROM categories WHERE id=?")
|
||||
.bind(id)
|
||||
.map(|row: MySqlRow| Category {
|
||||
id: row.get("id"),
|
||||
name: row.get("name"),
|
||||
})
|
||||
.fetch_optional(&self.db_pool)
|
||||
.await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn get_categories(&self) -> 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(&self.db_pool)
|
||||
.await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn get_user(&self, identifier: UserIdentifier) -> Result<Option<User>> {
|
||||
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<Self> = query
|
||||
.map(|row: MySqlRow| Self {
|
||||
let q: Option<User> = query
|
||||
.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_optional(pool)
|
||||
.fetch_optional(&self.db_pool)
|
||||
.await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Self>> {
|
||||
let q: Vec<Self> = 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 {
|
||||
pub async fn get_users(&self) -> Result<Vec<User>> {
|
||||
let q: Vec<User> = 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| 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?;
|
||||
.fetch_all(&self.db_pool).await?;
|
||||
Ok(q)
|
||||
}
|
||||
|
||||
pub async fn check_token(token: &String, pool: &MySqlPool) -> Result<Option<Self>> {
|
||||
let user = Self::get(UserIdentifier::Token(token.clone()), pool).await?;
|
||||
pub async fn check_token(&self, token: &String) -> Result<Option<User>> {
|
||||
let user = self.get_user(UserIdentifier::Token(token.clone())).await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
pub async fn add_meme_sql(
|
||||
&self,
|
||||
user: &User,
|
||||
file: &IPFSFile,
|
||||
ip: &String,
|
||||
category: &Category,
|
||||
) -> Result<u64> {
|
||||
let mut tx = self.db_pool.begin().await?;
|
||||
sqlx::query("INSERT INTO memes (filename, user, category, timestamp, ip, cid) VALUES (?, ?, ?, NOW(), ?, ?)")
|
||||
.bind(&file.name)
|
||||
.bind(&user.id)
|
||||
.bind(&category.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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,37 +2,13 @@ use std::convert::Infallible;
|
|||
|
||||
use axum::{
|
||||
body::{Bytes, Full},
|
||||
extract::{multipart::MultipartError, rejection::QueryRejection},
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use reqwest::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::models::ErrorResponse;
|
||||
use crate::error::ServiceError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum APIError {
|
||||
#[error("SQL error: {0}")]
|
||||
Sql(#[from] sqlx::Error),
|
||||
#[error("Multipart form error: {0}")]
|
||||
Multipart(#[from] MultipartError),
|
||||
#[error("{0}")]
|
||||
BadRequest(String),
|
||||
#[error("{0}")]
|
||||
Unauthorized(String),
|
||||
#[error("{0}")]
|
||||
Forbidden(String),
|
||||
#[error("{0}")]
|
||||
NotFound(String),
|
||||
#[error("{0}")]
|
||||
Internal(String),
|
||||
#[error("JMService error: {0}")]
|
||||
Service(#[from] ServiceError),
|
||||
#[error("Query rejection: {0}")]
|
||||
Query(#[from] QueryRejection),
|
||||
}
|
||||
use crate::error::APIError;
|
||||
|
||||
impl ErrorResponse {
|
||||
fn new(status: StatusCode, message: Option<String>) -> Self {
|
||||
|
@ -63,6 +39,7 @@ impl IntoResponse for APIError {
|
|||
APIError::Internal(err) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err)),
|
||||
APIError::Service(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
|
||||
APIError::Query(_) => ErrorResponse::new(StatusCode::BAD_REQUEST, None),
|
||||
APIError::Decode(_) => ErrorResponse::new(StatusCode::BAD_REQUEST, None),
|
||||
};
|
||||
let status = res.status;
|
||||
(status, Json(res)).into_response()
|
||||
|
|
|
@ -7,7 +7,7 @@ use axum::extract::{FromRequest, RequestParts};
|
|||
pub use routes::routes;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
use self::error::APIError;
|
||||
use crate::error::APIError;
|
||||
|
||||
pub struct Query<T>(pub T);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
|
||||
use crate::models::{Category, Meme, User, UserIdentifier};
|
||||
use crate::models::{Category, Meme, MemeOptions, User, UserIdentifier};
|
||||
|
||||
fn serialize_status<S>(x: &StatusCode, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
@ -124,3 +124,23 @@ impl From<UserIDQuery> for UserIdentifier {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MemeFilter {
|
||||
pub category: Option<String>,
|
||||
pub user: Option<String>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
impl From<MemeFilter> for MemeOptions {
|
||||
fn from(filter: MemeFilter) -> Self {
|
||||
Self {
|
||||
category: filter.category,
|
||||
user_id: None,
|
||||
username: filter.user,
|
||||
search: filter.search,
|
||||
limit: None,
|
||||
after: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use crate::ipfs::IPFSFile;
|
||||
use crate::lib::ExtractIP;
|
||||
use crate::models::{Category, Meme, MemeFilter, User};
|
||||
use crate::v1::models::*;
|
||||
use crate::JMService;
|
||||
|
||||
|
@ -10,18 +9,17 @@ use axum::response::IntoResponse;
|
|||
use axum::routing::BoxRoute;
|
||||
use axum::{Json, Router};
|
||||
use hyper::StatusCode;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use super::error::APIError;
|
||||
use super::Query;
|
||||
use crate::error::APIError;
|
||||
|
||||
async fn meme(
|
||||
Query(params): Query<MemeIDQuery>,
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
let meme = V1Meme::new(
|
||||
Meme::get(params.id, &db_pool)
|
||||
service
|
||||
.get_meme(params.id)
|
||||
.await?
|
||||
.ok_or_else(|| APIError::NotFound("Meme not found".to_string()))?,
|
||||
service.cdn_url(),
|
||||
|
@ -35,10 +33,10 @@ async fn meme(
|
|||
|
||||
async fn memes(
|
||||
Query(params): Query<MemeFilter>,
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
let memes = Meme::get_all(params, &db_pool)
|
||||
let memes = service
|
||||
.get_memes(params.into())
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|meme| V1Meme::new(meme, service.cdn_url()))
|
||||
|
@ -52,9 +50,10 @@ async fn memes(
|
|||
|
||||
async fn category(
|
||||
Query(params): Query<IDQuery>,
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
let category = Category::get(¶ms.id, &db_pool)
|
||||
let category = service
|
||||
.get_category(¶ms.id)
|
||||
.await?
|
||||
.ok_or_else(|| APIError::NotFound("Category not found".to_string()))?;
|
||||
Ok(Json(CategoryResponse {
|
||||
|
@ -65,9 +64,9 @@ async fn category(
|
|||
}
|
||||
|
||||
async fn categories(
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
let categories = Category::get_all(&db_pool).await?;
|
||||
let categories = service.get_categories().await?;
|
||||
Ok(Json(CategoriesResponse {
|
||||
status: 200,
|
||||
error: None,
|
||||
|
@ -77,9 +76,10 @@ async fn categories(
|
|||
|
||||
async fn user(
|
||||
Query(params): Query<UserIDQuery>,
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
let user = User::get(params.into(), &db_pool)
|
||||
let user = service
|
||||
.get_user(params.into())
|
||||
.await?
|
||||
.ok_or_else(|| APIError::NotFound("User not found".to_string()))?;
|
||||
Ok(Json(UserResponse {
|
||||
|
@ -89,8 +89,8 @@ async fn user(
|
|||
}))
|
||||
}
|
||||
|
||||
async fn users(Extension(db_pool): Extension<MySqlPool>) -> Result<impl IntoResponse, APIError> {
|
||||
let users = User::get_all(&db_pool).await?;
|
||||
async fn users(Extension(service): Extension<JMService>) -> Result<impl IntoResponse, APIError> {
|
||||
let users = service.get_users().await?;
|
||||
Ok(Json(UsersResponse {
|
||||
status: 200,
|
||||
error: None,
|
||||
|
@ -100,10 +100,12 @@ async fn users(Extension(db_pool): Extension<MySqlPool>) -> Result<impl IntoResp
|
|||
|
||||
async fn random(
|
||||
Query(params): Query<MemeFilter>,
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
let random = V1Meme::new(Meme::get_random(params, &db_pool).await?, service.cdn_url());
|
||||
let random = V1Meme::new(
|
||||
service.get_random_meme(params.into()).await?,
|
||||
service.cdn_url(),
|
||||
);
|
||||
Ok(Json(MemeResponse {
|
||||
status: 200,
|
||||
error: None,
|
||||
|
@ -113,7 +115,6 @@ async fn random(
|
|||
|
||||
async fn upload(
|
||||
ContentLengthLimit(mut form): ContentLengthLimit<Multipart, { 1024 * 1024 * 1024 }>,
|
||||
Extension(db_pool): Extension<MySqlPool>,
|
||||
Extension(service): Extension<JMService>,
|
||||
ExtractIP(ip): ExtractIP,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
|
@ -134,7 +135,7 @@ async fn upload(
|
|||
APIError::BadRequest("A file field has no filename".to_string())
|
||||
})?
|
||||
.to_string();
|
||||
let file = service.add(field.bytes().await?, filename).await?;
|
||||
let file = service.ipfs_add(field.bytes().await?, filename).await?;
|
||||
files.push(file);
|
||||
}
|
||||
_ => (),
|
||||
|
@ -143,7 +144,8 @@ async fn upload(
|
|||
|
||||
let token = token.ok_or_else(|| APIError::Unauthorized("Missing token".to_string()))?;
|
||||
let category = category.ok_or_else(|| APIError::BadRequest("Missing category".to_string()))?;
|
||||
let user = User::check_token(&token, &db_pool)
|
||||
let user = service
|
||||
.check_token(&token)
|
||||
.await?
|
||||
.ok_or_else(|| APIError::Forbidden("token not existing".to_string()))?;
|
||||
let total = (user.dayuploads as isize) + (files.len() as isize);
|
||||
|
@ -152,7 +154,8 @@ async fn upload(
|
|||
return Err(APIError::Forbidden("Upload limit reached".to_string()));
|
||||
}
|
||||
|
||||
let cat = Category::get(&category, &db_pool)
|
||||
let cat = service
|
||||
.get_category(&category)
|
||||
.await?
|
||||
.ok_or_else(|| APIError::BadRequest("Category not existing".to_string()))?;
|
||||
|
||||
|
@ -161,7 +164,7 @@ async fn upload(
|
|||
let mut links: Vec<String> = vec![];
|
||||
|
||||
for f in files {
|
||||
let res = cat.add_meme(&user, &f, &ip, &db_pool).await?;
|
||||
let res = service.add_meme_sql(&user, &f, &ip, &cat).await?;
|
||||
|
||||
if res == 0 {
|
||||
return Err(APIError::Internal("Database insertion error".to_string()));
|
||||
|
@ -175,7 +178,7 @@ async fn upload(
|
|||
res,
|
||||
)
|
||||
.await?;
|
||||
service.pin(f.hash).await?;
|
||||
service.ipfs_pin(f.hash).await?;
|
||||
links.push(format!(
|
||||
"{}/{}/{}",
|
||||
service.cdn_url(),
|
||||
|
|
4
src/v2/mod.rs
Normal file
4
src/v2/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
mod models;
|
||||
mod routes;
|
||||
|
||||
pub use routes::routes;
|
54
src/v2/models.rs
Normal file
54
src/v2/models.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use crate::models::{Meme, User};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct V2Meme {
|
||||
pub id: i32,
|
||||
pub filename: String,
|
||||
pub ipfs: String,
|
||||
pub category: String,
|
||||
pub user: String,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct V2User {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub dayuploads: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CDNEntry {
|
||||
pub directories: Vec<String>,
|
||||
pub files: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CDNFile {
|
||||
pub cid: String,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
impl From<Meme> for V2Meme {
|
||||
fn from(meme: Meme) -> Self {
|
||||
Self {
|
||||
id: meme.id,
|
||||
filename: meme.filename,
|
||||
category: meme.category,
|
||||
user: meme.userid,
|
||||
timestamp: meme.timestamp,
|
||||
ipfs: meme.ipfs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<User> for V2User {
|
||||
fn from(user: User) -> Self {
|
||||
Self {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
dayuploads: user.dayuploads,
|
||||
}
|
||||
}
|
||||
}
|
127
src/v2/routes.rs
Normal file
127
src/v2/routes.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
handler::get,
|
||||
response::IntoResponse,
|
||||
routing::BoxRoute,
|
||||
Json, Router,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::APIError,
|
||||
models::{MemeOptions, UserIdentifier},
|
||||
JMService,
|
||||
};
|
||||
|
||||
use super::models::{V2Meme, V2User};
|
||||
|
||||
async fn get_meme(
|
||||
Path(meme_id): Path<i32>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
Ok(Json(V2Meme::from(
|
||||
service
|
||||
.get_meme(meme_id)
|
||||
.await?
|
||||
.ok_or_else(|| APIError::NotFound("Meme not found".to_string()))?,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn get_memes(
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
Ok(Json(
|
||||
service
|
||||
.get_memes(MemeOptions::empty())
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(V2Meme::from)
|
||||
.collect::<Vec<V2Meme>>(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_category(
|
||||
Path(category_id): Path<String>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
Ok(Json(service.get_category(&category_id).await?.ok_or_else(
|
||||
|| APIError::NotFound("Category not found".to_string()),
|
||||
)?))
|
||||
}
|
||||
|
||||
async fn get_categories(
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
Ok(Json(service.get_categories().await?))
|
||||
}
|
||||
|
||||
async fn get_user(
|
||||
Path(user_id): Path<String>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
Ok(Json(V2User::from(
|
||||
service
|
||||
.get_user(UserIdentifier::Id(user_id))
|
||||
.await?
|
||||
.ok_or_else(|| APIError::NotFound("User not found".to_string()))?,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn get_users(
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
Ok(Json(
|
||||
service
|
||||
.get_users()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(V2User::from)
|
||||
.collect::<Vec<V2User>>(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_user_memes(
|
||||
Path(user_id): Path<String>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
Ok(Json(
|
||||
service
|
||||
.get_memes(MemeOptions {
|
||||
category: None,
|
||||
user_id: Some(user_id),
|
||||
username: None,
|
||||
search: None,
|
||||
limit: None,
|
||||
after: None,
|
||||
})
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(V2Meme::from)
|
||||
.collect::<Vec<V2Meme>>(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn get_user_meme(
|
||||
Path((user_id, filename)): Path<(String, String)>,
|
||||
Extension(service): Extension<JMService>,
|
||||
) -> Result<impl IntoResponse, APIError> {
|
||||
let decoded = urlencoding::decode(&filename)?.into_owned();
|
||||
Ok(Json(V2Meme::from(
|
||||
service
|
||||
.get_user_meme(user_id, decoded)
|
||||
.await?
|
||||
.ok_or_else(|| APIError::NotFound("Meme not found".to_string()))?,
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn routes() -> Router<BoxRoute> {
|
||||
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))
|
||||
.boxed()
|
||||
}
|
Loading…
Reference in a new issue