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