initial commit
This commit is contained in:
commit
b7a2265e9b
|
@ -0,0 +1 @@
|
||||||
|
/target
|
|
@ -0,0 +1,13 @@
|
||||||
|
[language-server.rust-analyzer]
|
||||||
|
command = "rust-analyzer"
|
||||||
|
environment = { "DATABASE_URL" = "postgres://critch:critch@localhost/critch" }
|
||||||
|
config = { cargo.features = "all" }
|
||||||
|
|
||||||
|
[[language]]
|
||||||
|
name = "rust"
|
||||||
|
file-types = ["rs", "html"]
|
||||||
|
|
||||||
|
[[language]]
|
||||||
|
name = "html"
|
||||||
|
auto-format = false
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "critch"
|
||||||
|
authors = ["cel <cel@bunny.garden>"]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
build = "src/build.rs"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
ructe = { version = "0.17.2", features = ["sass", "mime03"] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
poem = { version = "3.1.3", features = ["session"] }
|
||||||
|
serde = "1.0.215"
|
||||||
|
sqlx = { version = "0.8.2", features = ["uuid", "postgres", "runtime-tokio"] }
|
||||||
|
tokio = { version = "1.41.1", features = ["full"] }
|
||||||
|
toml = { version = "0.8.19", features = ["parse"] }
|
||||||
|
uuid = "1.11.0"
|
|
@ -0,0 +1,4 @@
|
||||||
|
admin_password = "clowning"
|
||||||
|
# site_password = "password"
|
||||||
|
files_dir = "./files"
|
||||||
|
database_connection = "postgres://critch:critch@localhost/critch"
|
|
@ -0,0 +1,43 @@
|
||||||
|
create extension if not exists "uuid-ossp";
|
||||||
|
|
||||||
|
create table artists (
|
||||||
|
id integer primary key generated always as identity,
|
||||||
|
artist_name varchar(128) not null unique,
|
||||||
|
bio text,
|
||||||
|
site varchar(256)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table artworks (
|
||||||
|
id integer primary key generated always as identity,
|
||||||
|
title varchar(256),
|
||||||
|
description text,
|
||||||
|
url_source varchar(256),
|
||||||
|
artist_id integer not null,
|
||||||
|
comment_number integer not null default 0,
|
||||||
|
foreign key (artist_id) references artists(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table comments (
|
||||||
|
id integer unique not null,
|
||||||
|
text text not null,
|
||||||
|
thread_id integer not null,
|
||||||
|
primary key (id, thread_id),
|
||||||
|
foreign key (thread_id) references artworks(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table comment_relations (
|
||||||
|
thread_id integer,
|
||||||
|
foreign key (thread_id) references artworks(id),
|
||||||
|
in_reply_to_id integer,
|
||||||
|
foreign key (in_reply_to_id) references comments(id),
|
||||||
|
comment_id integer,
|
||||||
|
foreign key (comment_id) references comments(id),
|
||||||
|
primary key (thread_id, in_reply_to_id, comment_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table files (
|
||||||
|
id uuid primary key default gen_random_uuid(),
|
||||||
|
alt_text text,
|
||||||
|
artwork_id integer,
|
||||||
|
foreign key (artwork_id) references artworks(id)
|
||||||
|
);
|
|
@ -0,0 +1,6 @@
|
||||||
|
pub struct Artist {
|
||||||
|
id: Option<usize>,
|
||||||
|
name: String,
|
||||||
|
bio: Option<String>,
|
||||||
|
site: Option<String>,
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
pub struct Artwork {
|
||||||
|
/// artwork id
|
||||||
|
id: Option<usize>,
|
||||||
|
/// name of the artwork
|
||||||
|
title: Option<String>,
|
||||||
|
/// description of the artwork
|
||||||
|
description: Option<String>,
|
||||||
|
/// source url of the artwork
|
||||||
|
url_source: Option<String>,
|
||||||
|
/// id of the artist
|
||||||
|
artist_id: usize,
|
||||||
|
/// ids of files
|
||||||
|
files: Vec<usize>,
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
use ructe::{Result, Ructe};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let mut ructe = Ructe::from_env()?;
|
||||||
|
ructe.statics()?.add_files("./static")?;
|
||||||
|
// .add_sass_file("./style.scss")?;
|
||||||
|
ructe.compile_templates("templates")
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
pub struct Comment {
|
||||||
|
/// id of the comment in the thread
|
||||||
|
id: Option<usize>,
|
||||||
|
/// text of the comment
|
||||||
|
text: String,
|
||||||
|
/// thread comment is in
|
||||||
|
thread: usize,
|
||||||
|
/// comments that are mentioned by the comment
|
||||||
|
in_reply_to: Vec<usize>,
|
||||||
|
/// comments that mention the comment
|
||||||
|
mentioned_by: Vec<usize>,
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::Read,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
admin_password: String,
|
||||||
|
site_password: Option<String>,
|
||||||
|
files_dir: std::path::PathBuf,
|
||||||
|
database_connection: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn from_file(path: &str) -> Result<Self> {
|
||||||
|
let path = PathBuf::from(path);
|
||||||
|
let mut config = String::new();
|
||||||
|
File::open(path)?.read_to_string(&mut config)?;
|
||||||
|
let config: Config = toml::from_str(&config)?;
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn admin_password(&self) -> &str {
|
||||||
|
&self.admin_password
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn site_password(&self) -> Option<&str> {
|
||||||
|
self.site_password.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn files_dir(&self) -> &Path {
|
||||||
|
self.files_dir.as_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn database_connection(&self) -> &str {
|
||||||
|
&self.database_connection
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
|
||||||
|
|
||||||
|
mod artworks;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Database(Pool<Postgres>);
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn new(connection_string: &str) -> Self {
|
||||||
|
let pool = PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(connection_string)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
|
||||||
|
|
||||||
|
Self(pool)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
IOError(std::io::Error),
|
||||||
|
TOMLError(toml::de::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::IOError(error) => write!(f, "IO Error: {}", error),
|
||||||
|
Error::TOMLError(error) => write!(f, "TOML deserialization error: {}", error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
Self::IOError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<toml::de::Error> for Error {
|
||||||
|
fn from(e: toml::de::Error) -> Self {
|
||||||
|
Self::TOMLError(e)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct File {
|
||||||
|
id: Option<Uuid>,
|
||||||
|
artwork: usize,
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
use config::Config;
|
||||||
|
use db::Database;
|
||||||
|
|
||||||
|
mod artist;
|
||||||
|
mod artwork;
|
||||||
|
mod comment;
|
||||||
|
pub mod config;
|
||||||
|
mod db;
|
||||||
|
mod error;
|
||||||
|
mod file;
|
||||||
|
pub mod routes;
|
||||||
|
mod ructe_poem;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, error::Error>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Critch {
|
||||||
|
db: Database,
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Critch {
|
||||||
|
pub async fn new(config: Config) -> Self {
|
||||||
|
let db = Database::new(config.database_connection()).await;
|
||||||
|
|
||||||
|
Self { db, config }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
use poem::{delete, post, put, EndpointExt};
|
||||||
|
use poem::{get, listener::TcpListener, Route, Server};
|
||||||
|
|
||||||
|
use critch::config::Config;
|
||||||
|
use critch::routes;
|
||||||
|
use critch::Critch;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), std::io::Error> {
|
||||||
|
let config = Config::from_file("./critch.toml").unwrap();
|
||||||
|
let state = Critch::new(config).await;
|
||||||
|
|
||||||
|
let app = Route::new()
|
||||||
|
.at(
|
||||||
|
"/admin",
|
||||||
|
post(routes::admin::login).get(routes::admin::get_login_form),
|
||||||
|
)
|
||||||
|
.at("/", get(routes::artworks::get))
|
||||||
|
.at(
|
||||||
|
"/artworks",
|
||||||
|
get(routes::artworks::get).post(routes::artworks::post),
|
||||||
|
)
|
||||||
|
.at(
|
||||||
|
"/artworks/:artwork",
|
||||||
|
get(routes::artworks::get)
|
||||||
|
.put(routes::artworks::put)
|
||||||
|
.delete(routes::artworks::delete),
|
||||||
|
)
|
||||||
|
.at(
|
||||||
|
"/artists",
|
||||||
|
get(routes::artists::get).post(routes::artists::post),
|
||||||
|
)
|
||||||
|
.at(
|
||||||
|
"/artists/:artist",
|
||||||
|
get(routes::artists::get)
|
||||||
|
.put(routes::artists::put)
|
||||||
|
.delete(routes::artists::delete),
|
||||||
|
)
|
||||||
|
.at(
|
||||||
|
"/artworks/:artwork/comments",
|
||||||
|
post(routes::artworks::comments::post).delete(routes::artworks::comments::delete),
|
||||||
|
)
|
||||||
|
.data(state);
|
||||||
|
|
||||||
|
Server::new(TcpListener::bind("0.0.0.0:3000"))
|
||||||
|
.run(app)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
use poem::handler;
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn login() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn get_login_form() {
|
||||||
|
todo!()
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
use poem::handler;
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn post() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn get() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn put() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn delete() {
|
||||||
|
todo!()
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
use poem::{handler, web::Path};
|
||||||
|
|
||||||
|
pub mod comments;
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn post() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn get() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn put() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn delete() {
|
||||||
|
todo!()
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
use poem::handler;
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn post() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn delete() {
|
||||||
|
todo!()
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod admin;
|
||||||
|
pub mod artists;
|
||||||
|
pub mod artworks;
|
|
@ -0,0 +1,27 @@
|
||||||
|
use poem::{http::StatusCode, Body, IntoResponse};
|
||||||
|
|
||||||
|
macro_rules! render {
|
||||||
|
($template:path) => {{
|
||||||
|
use $crate::axum_ructe::Render;
|
||||||
|
Render(|o| $template(o))
|
||||||
|
}};
|
||||||
|
($template:path, $($arg:expr),* $(,)*) => {{
|
||||||
|
use $crate::axum_ructe::Render;
|
||||||
|
Render(move |o| $template(o, $($arg),*))
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Render<T: FnOnce(&mut Vec<u8>) -> std::io::Result<()> + Send>(pub T);
|
||||||
|
|
||||||
|
impl<T: FnOnce(&mut Vec<u8>) -> std::io::Result<()> + Send> IntoResponse for Render<T> {
|
||||||
|
fn into_response(self) -> poem::Response {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
match self.0(&mut buf) {
|
||||||
|
Ok(()) => Body::from_vec(buf).into_response(),
|
||||||
|
Err(_e) => {
|
||||||
|
// TODO: logging
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Render failed").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue