~skye/pulver/src/act.rs
view raw
use axum::{
extract::{Multipart, Path},
response::{Html, IntoResponse},
Form,
};
use lazy_static::lazy_static;
use sbcom::macros::*;
use sbse::{err_fmt, fatal};
use serde::Deserialize;
use crate::{db::Db, fmt_unix, macros::*, truncate, Res, CFG};
use std::{
collections::{HashMap, HashSet},
fs::File,
io::Read,
};
lazy_static! {
static ref BOARDS: HashSet<String> =
match Db::new().expect("new db").get_boards() {
Ok(x) => x,
Err(e) => fatal!("error getting boards from database: {e}"),
};
}
macro_rules! page {
{ ($($t:tt)*), ($($b:tt)*) } => {{
Ok(Html(format!(
r#"
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{t} :: {title}</title>
<link rel="stylesheet" href="/style.css" type="text/css">
</head>
<body>
<div id="head">
{boards}
</div>
<div id="body">
{b}
</div>
<div id="foot">
<a href="/">{title}</a> | VERSION {vers}
</div>
</body>
</html>
"#,
t = format!($($t)*),
b = format!($($b)*),
title = CFG.title,
vers = CFG.version,
boards = BOARDS.iter()
.map(|x| format!(r#"<a href="/{x}/">/{x}/</a>"#))
.collect::<Vec<_>>()
.join(" / "),
)))
}};
}
macro_rules! log_page {
($($x:tt)*) => {{
log!("{}: {}", "viewing page".cyan(), format!($($x)*));
}};
}
macro_rules! mk_page {
{ $n:ident($($a:tt)*) -> $t:ty => $x:expr } => {
pub async fn $n($($a)*) -> Result<$t, String> {
log_page!("{}", stringify!($n));
$x
}
};
}
macro_rules! redirect {
($($x:tt)*) => {{
use axum::response::Redirect;
Ok(Redirect::to(&format!($($x)*)))
}};
}
type HtmlStr = Html<String>;
fn verify_form(v: &[(&str, &str, (usize, usize))]) -> Res<()> {
for (n, s, (min, max)) in v.into_iter() {
let l = s.len();
if l < *min || l > *max {
return err_fmt!(
"invalid form parameter: {n} must be between {} and {} chars",
min,
max
);
}
}
Ok(())
}
macro_rules! verify_form {
($f:expr, $(($n:ident, ($min:expr, $max:expr))),* ) => {{
verify_form(&[$((stringify!($n), &$f.$n, ($min, $max))),*])?;
}};
}
async fn decode_multipart(
m: &mut Multipart,
w: &[&str],
i: &str,
) -> Res<(HashMap<String, String>, Option<Vec<u8>>)> {
let mut h = HashMap::new();
let mut r = None;
while let Some(f) = {
ex!(m.next_field().await, Err(e) => "failed to get next field: {e}")?
} {
let n = un!(f.name(), "failed to get field name")?.to_string();
if w.contains(&n.as_str()) {
h.insert(
n.to_string(),
ex!(
f.text().await,
Err(e) => "failed to get text from field {n}: {e}"
)?,
);
} else if n == i {
let b = ex!(
f.bytes().await,
Err(e) => "failed to get bytes from multipart: {e}"
)?;
/* TODO: inefficient type system hackery */
r = match &b.to_vec()[..] {
&[] => None,
b => Some(b.to_vec())
}
}
}
Ok((h, r))
}
mk_page! { index() -> HtmlStr => {
page! {
("home"),
(
r#"
<h1>{}</h1>
<p>{}</p>
<hr>
"#,
CFG.title,
CFG.tag,
)
}
} }
mk_page! { data(Path(p): Path<String>) -> impl IntoResponse => {
verify_form(&[("path", &p, (1, 64))])?;
let f_p = format!("{}/{p}", CFG.img);
let mut f = ex!(File::open(&f_p), Err(e) => "failed to open file: {e}")?;
let mut b = Vec::<u8>::new();
ex!(f.read_to_end(&mut b), Err(e) => "failed to decode image: {e}")?;
Ok(b)
} }
mk_page! { board(Path(b): Path<String>) -> HtmlStr => {
let mut db = Db::new()?;
if BOARDS.contains(&b) {
page! {
("/{b}/"),
(
r#"
<h1>/{b}/</h1>
<div id="mkthread">
<form action="/act/mkthread" method="post" enctype="multipart/form-data">
<table>
<tr>
<td>username</td>
<td>
<input
type="text"
name="auth"
placeholder="anon"
>
</td>
</tr>
<tr>
<td>title</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>content</td>
<td><textarea name="cont"></textarea></td>
</tr>
</table>
<input type="file" name="img">
<input type="hidden" name="board" value="{b}">
<input type="submit" value="make thread">
</form>
</div>
<hr>
<table>
{}
</table>
"#,
db.get_threads(&b)?.into_iter()
.map(|x| format!(
r#"
<tr class="thread">
<td><a href="/{b}/{}">{}</a></td>
<td>{}</td>
<td>{}</td>
</tr>
"#,
&x.id,
x.name,
fmt_unix(x.time),
truncate(&x.cont, 30),
))
.collect::<String>()
)
}
} else {
err_fmt!("board not found")
}
} }
mk_page! { thread(Path((b, i)): Path<(String, String)>) -> HtmlStr => {
let mut db = Db::new()?;
let t = db.get_thread(&b, &i)?;
log!("t: {t:?}");
let mk_post = |a: &str, c: &str, t: i64, i: Option<String>| format!(
r#"
<div class="post">
<p>{a} :: {}</p>
<div class="img">{}</div>
<p>{c}</p>
</div>
"#,
fmt_unix(t),
match i {
Some(x) => format!(
r#"<a href="/data/{x}"><img src="/data/{x}"></a>"#
),
None => String::new(),
}
);
page! {
("{} :: /{b}/", &t.name),
(
r#"
<h1>{n}</h1>
<form action="/act/mkpost" method="post" enctype="multipart/form-data">
<table>
<tr>
<td>username</td>
<td><input
type="text"
name="auth"
placeholder="anon"
></td>
</tr>
<tr>
<td>comment</td>
<td><textarea name="cont"></textarea></td>
</tr>
</table>
<input type="hidden" name="board" value="{b}">
<input type="hidden" name="thread" value="{i}">
<input type="file" name="img">
<input type="submit" value="make post">
</form>
<p><a href="/{b}/">go back</a></p>
<hr>
<div id="posts">
{body}
{p}
</div>
"#,
n = &t.name,
b = &b,
i = &i,
body = mk_post(&t.auth, &t.cont, t.time, Some(t.img)),
p = db.get_posts(&b, &t.id)?.into_iter().map(|x| {
mk_post(&x.auth, &x.cont, x.time, x.img)
}).collect::<String>(),
)
}
} }
#[derive(Deserialize, Debug)]
pub struct Thread {
pub name: String,
pub auth: String,
pub cont: String,
pub board: String,
}
mk_page! { mkthread(mut m: Multipart) -> impl IntoResponse => {
let (frm, i) = match decode_multipart(
&mut m,
&["name", "auth", "cont", "board"],
"img"
).await? {
(f, Some(i)) => (f, i),
(_, None) => return err_fmt!("no image submitted in form"),
};
let g = |x| match frm.get(x) {
Some(x) => Ok(x.to_string()),
None => err_fmt!("key {x} not found in form"),
};
let f = Thread {
name: g("name")?,
auth: g("auth")?,
cont: g("cont")?,
board: g("board")?,
};
verify_form!(
f,
(name, (1, 32)),
(auth, (0, 15)),
(cont, (1, 64)),
(board, (1, 3))
);
let mut db = Db::new()?;
let a = &f.auth;
let b = &f.board;
db.new_thread(b, &f.name, if a == "" { "anon" } else { a }, &f.cont, &i)?;
redirect!("/{b}/")
} }
#[derive(Deserialize, Debug)]
pub struct Post {
pub auth: String,
pub cont: String,
pub board: String,
pub thread: String,
}
mk_page! { mkpost(mut m: Multipart) -> impl IntoResponse => {
let (frm, i) = decode_multipart(
&mut m,
&["auth", "cont", "board", "thread"],
"img"
).await?;
let g = |x| match frm.get(x) {
Some(x) => Ok(x.to_string()),
None => err_fmt!("no {x} in form")
};
let f = Post {
auth: g("auth")?,
cont: g("cont")?,
board: g("board")?,
thread: g("thread")?,
};
verify_form!(
f,
(auth, (0, 15)),
(cont, (1, 64)),
(board, (1, 3)),
(thread, (1, 64))
);
let mut db = Db::new()?;
let a = &f.auth;
let b = &f.board;
let t = &f.thread;
db.new_post(b, t, if a == "" { "anon" } else { a }, &f.cont, i)?;
redirect!("/{b}/{}", t)
} }