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
|
- Refactoring
|
||||||
- Allows bots to know, when a new meme was uploaded
|
- Initial V2 API
|
||||||
- First step towards decentralization
|
- 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},
|
header::{HeaderName, CONTENT_LENGTH},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
};
|
};
|
||||||
use sqlx::MySqlPool;
|
|
||||||
|
|
||||||
use crate::JMService;
|
use crate::JMService;
|
||||||
|
|
||||||
|
@ -35,12 +34,11 @@ pub fn routes() -> Router<BoxRoute> {
|
||||||
|
|
||||||
async fn image(
|
async fn image(
|
||||||
Path((user, filename)): Path<(String, String)>,
|
Path((user, filename)): Path<(String, String)>,
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
|
||||||
Extension(service): Extension<JMService>,
|
Extension(service): Extension<JMService>,
|
||||||
) -> Result<impl IntoResponse, CDNError> {
|
) -> Result<impl IntoResponse, CDNError> {
|
||||||
let filename = urlencoding::decode(&filename)?.into_owned();
|
let filename = urlencoding::decode(&filename)?.into_owned();
|
||||||
let cid = sql::get_cid(user, filename.clone(), &db_pool).await?;
|
let cid = sql::get_cid(user, filename.clone(), &service.db_pool).await?;
|
||||||
let res = service.cat(cid).await?;
|
let res = service.ipfs_cat(cid).await?;
|
||||||
let clength = res
|
let clength = res
|
||||||
.headers()
|
.headers()
|
||||||
.get(HeaderName::from_static("x-content-length"))
|
.get(HeaderName::from_static("x-content-length"))
|
||||||
|
@ -58,11 +56,8 @@ async fn image(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn users(
|
async fn users(Extension(service): Extension<JMService>) -> Result<impl IntoResponse, CDNError> {
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
let users = sql::get_users(&service.db_pool).await?;
|
||||||
Extension(service): Extension<JMService>,
|
|
||||||
) -> Result<impl IntoResponse, CDNError> {
|
|
||||||
let users = sql::get_users(&db_pool).await?;
|
|
||||||
Ok(HtmlTemplate(DirTemplate {
|
Ok(HtmlTemplate(DirTemplate {
|
||||||
entries: users,
|
entries: users,
|
||||||
prefix: service.cdn_url(),
|
prefix: service.cdn_url(),
|
||||||
|
@ -72,9 +67,9 @@ async fn users(
|
||||||
|
|
||||||
async fn memes(
|
async fn memes(
|
||||||
Path(user): Path<String>,
|
Path(user): Path<String>,
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
Extension(service): Extension<JMService>,
|
||||||
) -> Result<impl IntoResponse, CDNError> {
|
) -> 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 {
|
Ok(HtmlTemplate(DirTemplate {
|
||||||
entries: memes,
|
entries: memes,
|
||||||
prefix: ".".to_string(),
|
prefix: ".".to_string(),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use sqlx::MySqlPool;
|
||||||
use std::{net::SocketAddr, sync::Arc};
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
use crate::{error::JMError, JMService, JMServiceInner};
|
use crate::{error::JMError, JMService, JMServiceInner};
|
||||||
|
@ -16,10 +17,11 @@ pub struct Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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()?;
|
let client = reqwest::ClientBuilder::new().user_agent("curl").build()?;
|
||||||
Ok(Arc::new(JMServiceInner {
|
Ok(Arc::new(JMServiceInner {
|
||||||
client,
|
client,
|
||||||
|
db_pool,
|
||||||
ipfs_url: self.ipfs_api.clone(),
|
ipfs_url: self.ipfs_api.clone(),
|
||||||
cdn_url: self.cdn.clone(),
|
cdn_url: self.cdn.clone(),
|
||||||
matrix_url: self.matrix_url.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 hyper::StatusCode;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::ParseError;
|
use url::ParseError;
|
||||||
|
@ -25,3 +28,27 @@ pub enum ServiceError {
|
||||||
#[error("Invalid response code: {0}")]
|
#[error("Invalid response code: {0}")]
|
||||||
InvalidResponse(StatusCode),
|
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 {
|
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
|
let request = self
|
||||||
.client
|
.client
|
||||||
.post(self.ipfs_url.join("/api/v0/cat")?)
|
.post(self.ipfs_url.join("/api/v0/cat")?)
|
||||||
|
@ -43,7 +43,7 @@ impl JMServiceInner {
|
||||||
Ok(request.send().await?)
|
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
|
let request = self
|
||||||
.client
|
.client
|
||||||
.post(self.ipfs_url.join("/api/v0/add")?)
|
.post(self.ipfs_url.join("/api/v0/add")?)
|
||||||
|
@ -54,7 +54,7 @@ impl JMServiceInner {
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pin(&self, cid: String) -> Result<(), ServiceError> {
|
pub async fn ipfs_pin(&self, cid: String) -> Result<(), ServiceError> {
|
||||||
let request = self
|
let request = self
|
||||||
.client
|
.client
|
||||||
.post(self.ipfs_url.join("/api/v0/pin/add")?)
|
.post(self.ipfs_url.join("/api/v0/pin/add")?)
|
||||||
|
|
|
@ -20,6 +20,7 @@ mod matrix;
|
||||||
mod models;
|
mod models;
|
||||||
mod sql;
|
mod sql;
|
||||||
mod v1;
|
mod v1;
|
||||||
|
mod v2;
|
||||||
|
|
||||||
#[derive(StructOpt)]
|
#[derive(StructOpt)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
|
@ -34,6 +35,7 @@ struct Opt {
|
||||||
|
|
||||||
pub struct JMServiceInner {
|
pub struct JMServiceInner {
|
||||||
client: Client,
|
client: Client,
|
||||||
|
db_pool: MySqlPool,
|
||||||
ipfs_url: Url,
|
ipfs_url: Url,
|
||||||
cdn_url: String,
|
cdn_url: String,
|
||||||
matrix_url: Url,
|
matrix_url: Url,
|
||||||
|
@ -50,12 +52,12 @@ async fn main() -> Result<(), JMError> {
|
||||||
let config = toml::from_slice::<Config>(&config)?;
|
let config = toml::from_slice::<Config>(&config)?;
|
||||||
|
|
||||||
let db_pool = MySqlPool::new(&config.database).await?;
|
let db_pool = MySqlPool::new(&config.database).await?;
|
||||||
let service = config.service()?;
|
let service = config.service(db_pool)?;
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/api/v1", v1::routes())
|
.nest("/api/v1", v1::routes())
|
||||||
|
.nest("/api/v2", v2::routes())
|
||||||
.nest("/cdn", cdn::routes())
|
.nest("/cdn", cdn::routes())
|
||||||
.layer(AddExtensionLayer::new(db_pool))
|
|
||||||
.layer(AddExtensionLayer::new(service))
|
.layer(AddExtensionLayer::new(service))
|
||||||
.layer(SetResponseHeaderLayer::<_, Request<Body>>::if_not_present(
|
.layer(SetResponseHeaderLayer::<_, Request<Body>>::if_not_present(
|
||||||
header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
header::ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct Meme {
|
pub struct Meme {
|
||||||
|
@ -33,9 +33,24 @@ pub enum UserIdentifier {
|
||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
pub struct MemeOptions {
|
||||||
pub struct MemeFilter {
|
|
||||||
pub category: Option<String>,
|
pub category: Option<String>,
|
||||||
pub user: Option<String>,
|
pub user_id: Option<String>,
|
||||||
|
pub username: Option<String>,
|
||||||
pub search: 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::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::mysql::MySqlRow;
|
||||||
use sqlx::{MySqlPool, Result, Row};
|
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 {
|
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(
|
pub async fn add_meme(
|
||||||
&self,
|
&self,
|
||||||
user: &User,
|
user: &User,
|
||||||
|
@ -104,42 +29,160 @@ impl Category {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl JMServiceInner {
|
||||||
pub async fn get(identifier: UserIdentifier, pool: &MySqlPool) -> Result<Option<Self>> {
|
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 {
|
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::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::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::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'"),
|
UserIdentifier::Null => sqlx::query("SELECT id, name, '0' AS hash, 0 AS uploads FROM users WHERE id = '000'"),
|
||||||
};
|
};
|
||||||
let q: Option<Self> = query
|
let q: Option<User> = query
|
||||||
.map(|row: MySqlRow| Self {
|
.map(|row: MySqlRow| User {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
name: row.get("name"),
|
name: row.get("name"),
|
||||||
userdir: row.get("id"),
|
userdir: row.get("id"),
|
||||||
tokenhash: row.get("hash"),
|
tokenhash: row.get("hash"),
|
||||||
dayuploads: row.get("uploads"),
|
dayuploads: row.get("uploads"),
|
||||||
})
|
})
|
||||||
.fetch_optional(pool)
|
.fetch_optional(&self.db_pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(q)
|
Ok(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all(pool: &MySqlPool) -> Result<Vec<Self>> {
|
pub async fn get_users(&self) -> Result<Vec<User>> {
|
||||||
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")
|
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| Self {
|
.map(|row: MySqlRow| User {
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
name: row.get("name"),
|
name: row.get("name"),
|
||||||
userdir: row.get("id"),
|
userdir: row.get("id"),
|
||||||
tokenhash: row.get("hash"),
|
tokenhash: row.get("hash"),
|
||||||
dayuploads: row.get("uploads"),
|
dayuploads: row.get("uploads"),
|
||||||
})
|
})
|
||||||
.fetch_all(pool).await?;
|
.fetch_all(&self.db_pool).await?;
|
||||||
Ok(q)
|
Ok(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_token(token: &String, pool: &MySqlPool) -> Result<Option<Self>> {
|
pub async fn check_token(&self, token: &String) -> Result<Option<User>> {
|
||||||
let user = Self::get(UserIdentifier::Token(token.clone()), pool).await?;
|
let user = self.get_user(UserIdentifier::Token(token.clone())).await?;
|
||||||
Ok(user)
|
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::{
|
use axum::{
|
||||||
body::{Bytes, Full},
|
body::{Bytes, Full},
|
||||||
extract::{multipart::MultipartError, rejection::QueryRejection},
|
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use super::models::ErrorResponse;
|
use super::models::ErrorResponse;
|
||||||
use crate::error::ServiceError;
|
use crate::error::APIError;
|
||||||
|
|
||||||
#[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),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ErrorResponse {
|
impl ErrorResponse {
|
||||||
fn new(status: StatusCode, message: Option<String>) -> Self {
|
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::Internal(err) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Some(err)),
|
||||||
APIError::Service(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
|
APIError::Service(_) => ErrorResponse::new(StatusCode::INTERNAL_SERVER_ERROR, None),
|
||||||
APIError::Query(_) => ErrorResponse::new(StatusCode::BAD_REQUEST, None),
|
APIError::Query(_) => ErrorResponse::new(StatusCode::BAD_REQUEST, None),
|
||||||
|
APIError::Decode(_) => ErrorResponse::new(StatusCode::BAD_REQUEST, None),
|
||||||
};
|
};
|
||||||
let status = res.status;
|
let status = res.status;
|
||||||
(status, Json(res)).into_response()
|
(status, Json(res)).into_response()
|
||||||
|
|
|
@ -7,7 +7,7 @@ use axum::extract::{FromRequest, RequestParts};
|
||||||
pub use routes::routes;
|
pub use routes::routes;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
use self::error::APIError;
|
use crate::error::APIError;
|
||||||
|
|
||||||
pub struct Query<T>(pub T);
|
pub struct Query<T>(pub T);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
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>
|
fn serialize_status<S>(x: &StatusCode, s: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
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::ipfs::IPFSFile;
|
||||||
use crate::lib::ExtractIP;
|
use crate::lib::ExtractIP;
|
||||||
use crate::models::{Category, Meme, MemeFilter, User};
|
|
||||||
use crate::v1::models::*;
|
use crate::v1::models::*;
|
||||||
use crate::JMService;
|
use crate::JMService;
|
||||||
|
|
||||||
|
@ -10,18 +9,17 @@ use axum::response::IntoResponse;
|
||||||
use axum::routing::BoxRoute;
|
use axum::routing::BoxRoute;
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use sqlx::MySqlPool;
|
|
||||||
|
|
||||||
use super::error::APIError;
|
|
||||||
use super::Query;
|
use super::Query;
|
||||||
|
use crate::error::APIError;
|
||||||
|
|
||||||
async fn meme(
|
async fn meme(
|
||||||
Query(params): Query<MemeIDQuery>,
|
Query(params): Query<MemeIDQuery>,
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
|
||||||
Extension(service): Extension<JMService>,
|
Extension(service): Extension<JMService>,
|
||||||
) -> Result<impl IntoResponse, APIError> {
|
) -> Result<impl IntoResponse, APIError> {
|
||||||
let meme = V1Meme::new(
|
let meme = V1Meme::new(
|
||||||
Meme::get(params.id, &db_pool)
|
service
|
||||||
|
.get_meme(params.id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| APIError::NotFound("Meme not found".to_string()))?,
|
.ok_or_else(|| APIError::NotFound("Meme not found".to_string()))?,
|
||||||
service.cdn_url(),
|
service.cdn_url(),
|
||||||
|
@ -35,10 +33,10 @@ async fn meme(
|
||||||
|
|
||||||
async fn memes(
|
async fn memes(
|
||||||
Query(params): Query<MemeFilter>,
|
Query(params): Query<MemeFilter>,
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
|
||||||
Extension(service): Extension<JMService>,
|
Extension(service): Extension<JMService>,
|
||||||
) -> Result<impl IntoResponse, APIError> {
|
) -> Result<impl IntoResponse, APIError> {
|
||||||
let memes = Meme::get_all(params, &db_pool)
|
let memes = service
|
||||||
|
.get_memes(params.into())
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|meme| V1Meme::new(meme, service.cdn_url()))
|
.map(|meme| V1Meme::new(meme, service.cdn_url()))
|
||||||
|
@ -52,9 +50,10 @@ async fn memes(
|
||||||
|
|
||||||
async fn category(
|
async fn category(
|
||||||
Query(params): Query<IDQuery>,
|
Query(params): Query<IDQuery>,
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
Extension(service): Extension<JMService>,
|
||||||
) -> Result<impl IntoResponse, APIError> {
|
) -> Result<impl IntoResponse, APIError> {
|
||||||
let category = Category::get(¶ms.id, &db_pool)
|
let category = service
|
||||||
|
.get_category(¶ms.id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| APIError::NotFound("Category not found".to_string()))?;
|
.ok_or_else(|| APIError::NotFound("Category not found".to_string()))?;
|
||||||
Ok(Json(CategoryResponse {
|
Ok(Json(CategoryResponse {
|
||||||
|
@ -65,9 +64,9 @@ async fn category(
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn categories(
|
async fn categories(
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
Extension(service): Extension<JMService>,
|
||||||
) -> Result<impl IntoResponse, APIError> {
|
) -> Result<impl IntoResponse, APIError> {
|
||||||
let categories = Category::get_all(&db_pool).await?;
|
let categories = service.get_categories().await?;
|
||||||
Ok(Json(CategoriesResponse {
|
Ok(Json(CategoriesResponse {
|
||||||
status: 200,
|
status: 200,
|
||||||
error: None,
|
error: None,
|
||||||
|
@ -77,9 +76,10 @@ async fn categories(
|
||||||
|
|
||||||
async fn user(
|
async fn user(
|
||||||
Query(params): Query<UserIDQuery>,
|
Query(params): Query<UserIDQuery>,
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
Extension(service): Extension<JMService>,
|
||||||
) -> Result<impl IntoResponse, APIError> {
|
) -> Result<impl IntoResponse, APIError> {
|
||||||
let user = User::get(params.into(), &db_pool)
|
let user = service
|
||||||
|
.get_user(params.into())
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| APIError::NotFound("User not found".to_string()))?;
|
.ok_or_else(|| APIError::NotFound("User not found".to_string()))?;
|
||||||
Ok(Json(UserResponse {
|
Ok(Json(UserResponse {
|
||||||
|
@ -89,8 +89,8 @@ async fn user(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn users(Extension(db_pool): Extension<MySqlPool>) -> Result<impl IntoResponse, APIError> {
|
async fn users(Extension(service): Extension<JMService>) -> Result<impl IntoResponse, APIError> {
|
||||||
let users = User::get_all(&db_pool).await?;
|
let users = service.get_users().await?;
|
||||||
Ok(Json(UsersResponse {
|
Ok(Json(UsersResponse {
|
||||||
status: 200,
|
status: 200,
|
||||||
error: None,
|
error: None,
|
||||||
|
@ -100,10 +100,12 @@ async fn users(Extension(db_pool): Extension<MySqlPool>) -> Result<impl IntoResp
|
||||||
|
|
||||||
async fn random(
|
async fn random(
|
||||||
Query(params): Query<MemeFilter>,
|
Query(params): Query<MemeFilter>,
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
|
||||||
Extension(service): Extension<JMService>,
|
Extension(service): Extension<JMService>,
|
||||||
) -> Result<impl IntoResponse, APIError> {
|
) -> 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 {
|
Ok(Json(MemeResponse {
|
||||||
status: 200,
|
status: 200,
|
||||||
error: None,
|
error: None,
|
||||||
|
@ -113,7 +115,6 @@ async fn random(
|
||||||
|
|
||||||
async fn upload(
|
async fn upload(
|
||||||
ContentLengthLimit(mut form): ContentLengthLimit<Multipart, { 1024 * 1024 * 1024 }>,
|
ContentLengthLimit(mut form): ContentLengthLimit<Multipart, { 1024 * 1024 * 1024 }>,
|
||||||
Extension(db_pool): Extension<MySqlPool>,
|
|
||||||
Extension(service): Extension<JMService>,
|
Extension(service): Extension<JMService>,
|
||||||
ExtractIP(ip): ExtractIP,
|
ExtractIP(ip): ExtractIP,
|
||||||
) -> Result<impl IntoResponse, APIError> {
|
) -> Result<impl IntoResponse, APIError> {
|
||||||
|
@ -134,7 +135,7 @@ async fn upload(
|
||||||
APIError::BadRequest("A file field has no filename".to_string())
|
APIError::BadRequest("A file field has no filename".to_string())
|
||||||
})?
|
})?
|
||||||
.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);
|
files.push(file);
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
@ -143,7 +144,8 @@ async fn upload(
|
||||||
|
|
||||||
let token = token.ok_or_else(|| APIError::Unauthorized("Missing token".to_string()))?;
|
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 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?
|
.await?
|
||||||
.ok_or_else(|| APIError::Forbidden("token not existing".to_string()))?;
|
.ok_or_else(|| APIError::Forbidden("token not existing".to_string()))?;
|
||||||
let total = (user.dayuploads as isize) + (files.len() as isize);
|
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()));
|
return Err(APIError::Forbidden("Upload limit reached".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cat = Category::get(&category, &db_pool)
|
let cat = service
|
||||||
|
.get_category(&category)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| APIError::BadRequest("Category not existing".to_string()))?;
|
.ok_or_else(|| APIError::BadRequest("Category not existing".to_string()))?;
|
||||||
|
|
||||||
|
@ -161,7 +164,7 @@ async fn upload(
|
||||||
let mut links: Vec<String> = vec![];
|
let mut links: Vec<String> = vec![];
|
||||||
|
|
||||||
for f in files {
|
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 {
|
if res == 0 {
|
||||||
return Err(APIError::Internal("Database insertion error".to_string()));
|
return Err(APIError::Internal("Database insertion error".to_string()));
|
||||||
|
@ -175,7 +178,7 @@ async fn upload(
|
||||||
res,
|
res,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
service.pin(f.hash).await?;
|
service.ipfs_pin(f.hash).await?;
|
||||||
links.push(format!(
|
links.push(format!(
|
||||||
"{}/{}/{}",
|
"{}/{}/{}",
|
||||||
service.cdn_url(),
|
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