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