initial commit

This commit is contained in:
cel 🌸 2024-11-13 20:00:15 +00:00
commit b7a2265e9b
23 changed files with 3051 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

13
.helix/languages.toml Normal file
View File

@ -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

2658
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
Cargo.toml Normal file
View File

@ -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"

4
critch.toml Normal file
View File

@ -0,0 +1,4 @@
admin_password = "clowning"
# site_password = "password"
files_dir = "./files"
database_connection = "postgres://critch:critch@localhost/critch"

View File

@ -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)
);

6
src/artist.rs Normal file
View File

@ -0,0 +1,6 @@
pub struct Artist {
id: Option<usize>,
name: String,
bio: Option<String>,
site: Option<String>,
}

14
src/artwork.rs Normal file
View File

@ -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>,
}

8
src/build.rs Normal file
View File

@ -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")
}

12
src/comment.rs Normal file
View File

@ -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>,
}

43
src/config.rs Normal file
View File

@ -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
}
}

1
src/db/artworks.rs Normal file
View File

@ -0,0 +1 @@

20
src/db/mod.rs Normal file
View File

@ -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)
}
}

30
src/error.rs Normal file
View File

@ -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)
}
}

7
src/file.rs Normal file
View File

@ -0,0 +1,7 @@
use uuid::Uuid;
#[derive(sqlx::FromRow)]
pub struct File {
id: Option<Uuid>,
artwork: usize,
}

28
src/lib.rs Normal file
View File

@ -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 }
}
}

49
src/main.rs Normal file
View File

@ -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(())
}

11
src/routes/admin.rs Normal file
View File

@ -0,0 +1,11 @@
use poem::handler;
#[handler]
pub async fn login() {
todo!()
}
#[handler]
pub async fn get_login_form() {
todo!()
}

21
src/routes/artists.rs Normal file
View File

@ -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!()
}

23
src/routes/artworks.rs Normal file
View File

@ -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!()
}

View File

@ -0,0 +1,11 @@
use poem::handler;
#[handler]
pub async fn post() {
todo!()
}
#[handler]
pub async fn delete() {
todo!()
}

3
src/routes/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod admin;
pub mod artists;
pub mod artworks;

27
src/ructe_poem.rs Normal file
View File

@ -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()
}
}
}
}