initial commit
This commit is contained in:
commit
f9f2a75e89
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/target
|
||||
/.idea
|
||||
Cargo.lock
|
||||
*.db*
|
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "hermit"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "*"
|
||||
sqlx = { version = "*", features = [ "runtime-actix-rustls", "sqlite" ] }
|
||||
serde = { version = "*", features = [ "derive" ] }
|
||||
ammonia = "*"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "z"
|
8
migrations/20221024052654_posts.sql
Normal file
8
migrations/20221024052654_posts.sql
Normal file
@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS posts
|
||||
(
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
parent_id INTEGER,
|
||||
author TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
);
|
173
src/main.rs
Normal file
173
src/main.rs
Normal file
@ -0,0 +1,173 @@
|
||||
use actix_web::{http::header, web, App, HttpResponse, HttpServer, Responder};
|
||||
use ammonia::clean;
|
||||
use serde::Deserialize;
|
||||
use sqlx::{
|
||||
sqlite::{Sqlite, SqlitePool},
|
||||
Pool,
|
||||
};
|
||||
use std::env;
|
||||
|
||||
struct HermitState {
|
||||
pool: Pool<Sqlite>,
|
||||
}
|
||||
|
||||
struct Post {
|
||||
id: i64,
|
||||
parent_id: Option<i64>,
|
||||
author: String,
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PostForm {
|
||||
author: String,
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
fn render(posts: Vec<Post>, details: bool) -> String {
|
||||
let mut page = r#"
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>booru</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/">Home</a>
|
||||
<form method="POST">
|
||||
<input type="text" placeholder="op" name="author"><br />
|
||||
<input type="text" placeholder="title" name="title"><br />
|
||||
<textarea placeholder="your post goes here!" name="content"></textarea><br />
|
||||
<input type="submit" value="Post!">
|
||||
</form>"#
|
||||
.to_owned();
|
||||
|
||||
for post in posts {
|
||||
page.push_str(&format!(
|
||||
"<a href=\"/{}\">💬{}</a> - {}<br />\n",
|
||||
post.id, post.title, post.author
|
||||
));
|
||||
if details {
|
||||
page.push_str(&format!("<p>{}</p>", post.content))
|
||||
}
|
||||
}
|
||||
|
||||
page.push_str(
|
||||
r#"
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
);
|
||||
|
||||
page
|
||||
}
|
||||
|
||||
async fn get(state: web::Data<HermitState>, post_id: Option<web::Path<u32>>) -> impl Responder {
|
||||
match post_id {
|
||||
None => {
|
||||
let posts = sqlx::query_as!(
|
||||
Post,
|
||||
r#"
|
||||
SELECT * FROM posts
|
||||
WHERE parent_id IS NULL
|
||||
ORDER BY id DESC
|
||||
"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
HttpResponse::Ok().body(render(posts, false))
|
||||
}
|
||||
Some(id) => {
|
||||
let post_id = id.into_inner();
|
||||
let posts = sqlx::query_as!(
|
||||
Post,
|
||||
r#"
|
||||
SELECT * FROM posts
|
||||
WHERE id = ?1 OR parent_id = ?1
|
||||
ORDER BY id ASC
|
||||
"#,
|
||||
post_id
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
HttpResponse::Ok().body(render(posts, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn post(
|
||||
state: web::Data<HermitState>,
|
||||
post_id: Option<web::Path<u32>>,
|
||||
form: web::Form<PostForm>,
|
||||
) -> impl Responder {
|
||||
let author = clean(form.author.as_str());
|
||||
if author.is_empty() {
|
||||
return HttpResponse::BadRequest().body("Author required");
|
||||
}
|
||||
let title = clean(form.title.as_str());
|
||||
let content = clean(form.content.as_str());
|
||||
|
||||
let id = match post_id {
|
||||
None => {
|
||||
if title.is_empty() {
|
||||
return HttpResponse::BadRequest().body("Title required for top posts");
|
||||
}
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO posts (author, title, content)
|
||||
VALUES (?, ?, ?)
|
||||
"#,
|
||||
author,
|
||||
title,
|
||||
content
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.unwrap()
|
||||
.last_insert_rowid()
|
||||
}
|
||||
Some(id) => {
|
||||
let post_id = id.into_inner();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO posts (parent_id, author, title, content)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"#,
|
||||
post_id,
|
||||
author,
|
||||
title,
|
||||
content
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
post_id as i64
|
||||
}
|
||||
};
|
||||
HttpResponse::Found()
|
||||
.insert_header((header::LOCATION, format!("/{}", id)))
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
let hermit = web::Data::new(HermitState {
|
||||
pool: SqlitePool::connect(&env::var("DATABASE_URL").expect("Please set DATABASE_URL."))
|
||||
.await
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new().app_data(hermit.clone()).service(
|
||||
web::resource(["/", "/{post_id}"])
|
||||
.route(web::get().to(get))
|
||||
.route(web::post().to(post)),
|
||||
)
|
||||
})
|
||||
.bind(("0.0.0.0", 8069))?
|
||||
.run()
|
||||
.await
|
||||
}
|
Loading…
Reference in New Issue
Block a user