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 = 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#" {t} :: {title}
{b}
"#, t = format!($($t)*), b = format!($($b)*), title = CFG.title, vers = CFG.version, boards = BOARDS.iter() .map(|x| format!(r#"/{x}/"#)) .collect::>() .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; 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, Option>)> { 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#"

{}

{}


"#, CFG.title, CFG.tag, ) } } } mk_page! { data(Path(p): Path) -> 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::::new(); ex!(f.read_to_end(&mut b), Err(e) => "failed to decode image: {e}")?; Ok(b) } } mk_page! { board(Path(b): Path) -> HtmlStr => { let mut db = Db::new()?; if BOARDS.contains(&b) { page! { ("/{b}/"), ( r#"

/{b}/

username
title
content

{}
"#, db.get_threads(&b)?.into_iter() .map(|x| format!( r#" {} {} {} "#, &x.id, x.name, fmt_unix(x.time), truncate(&x.cont, 30), )) .collect::() ) } } 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| format!( r#"

{a} :: {}

{}

{c}

"#, fmt_unix(t), match i { Some(x) => format!( r#""# ), None => String::new(), } ); page! { ("{} :: /{b}/", &t.name), ( r#"

{n}

username
comment

go back


{body} {p}
"#, 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::(), ) } } } #[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) } }