~skye/pulver/src/db.rs

view raw


use image::ImageReader;
use redis::{Client, Commands, Connection};
use sbcom::macros::*;
use sbse::err_fmt;
use uuid::{ContextV7, Timestamp, Uuid};

use crate::{macros::*, now_timestamp, Res, CFG};
use std::{
    collections::HashSet,
    fs::File,
    io::{Cursor, Write},
};

/** we wrap password functions in case we want to change the algo or whatever */
#[allow(dead_code)]
mod pw {
    use crate::Res;
    use pwhash::bcrypt;
    use sbse::err_fmt;

    pub fn mk_hash(p: &str) -> Res<String> {
        match bcrypt::hash(p) {
            Ok(x) => Ok(x),
            Err(e) => err_fmt!("failed to hash password: {e}"),
        }
    }

    pub fn is_hash(p: &str, h: &str) -> bool {
        bcrypt::verify(p, h)
    }
}

macro_rules! db_log_fmt {
    ($($x:tt)*) => {{
        log_fmt!(" db".purple(), $($x)*)
    }};
}

macro_rules! db_log {
    ($($x:tt)*) => {{
        println!("{}", db_log_fmt!($($x)*));
    }};
}

macro_rules! sadd {
    ($self:expr, $x:expr, $y:expr) => {{
        match $self.con.sadd($x, $y) {
            Ok(()) => Ok(()),
            Err(e) => err_fmt!("failed to SADD({},{}): {e}", $x, $y),
        }
    }};
}

macro_rules! hset {
    ($self:expr, $x:expr, $y:expr) => {{
        match $self.con.hset_multiple($x, $y) {
            Ok(()) => Ok(()),
            Err(e) => err_fmt!("failed to HSET: {e}"),
        }
    }};
}

macro_rules! smembers {
    ($self:expr, $x:expr) => {{
        match $self.con.smembers($x) {
            Ok(x) => Ok(x),
            Err(e) => err_fmt!("failed to SMEMBERS({}): {e}", $x),
        }
    }};
}

macro_rules! hget {
    ($self:expr, $x:expr, $y:expr) => {{
        match $self.con.hget($x, $y) {
            Ok(x) => Ok(x),
            Err(e) => err_fmt!("failed to HGET({},{}): {e}", $x, $y),
        }
    }};
}

pub fn write_img(n: &str, i: &[u8]) -> Res<()> {
    let img = ex!(
        ImageReader::new(Cursor::new(i)).with_guessed_format(),
        Err(e) => "failed to open image: {e}"
    )?;
    ex!(img.decode(), Err(e) => "failed to decode image: {e}")?;

    let p = format!("{}/{}", &CFG.img, n);
    let mut f = ex!(
        File::create(&p),
        Err(e) => "failed to open file {p}: {e}"
    )?;
    ex!(f.write_all(i), Err(e) => "failed to write file {p}: {e}")
}

#[derive(Clone, Debug)]
pub struct Thread {
    pub id: String,
    pub time: i64,
    pub name: String,
    pub auth: String,
    pub cont: String,
    pub img: String,
}

#[derive(Clone, Debug)]
pub struct Post {
    pub id: String,
    pub time: i64,
    pub auth: String,
    pub cont: String,
    pub img: Option<String>,
}

pub struct Db {
    pub con: Connection,
}

impl Db {
    pub fn new() -> Res<Self> {
        match Client::open("redis://127.0.0.1/") {
            Ok(c) => match c.get_connection() {
                Ok(c) => Ok(Self { con: c }),
                Err(e) => err_fmt!("failed to connect to database: {e}"),
            },
            Err(e) => err_fmt!("failed to open database: {e}"),
        }
    }

    pub fn new_thread<B, N, A, C>(
        &mut self,
        b: B,
        n: N,
        a: A,
        c: C,
        i: &[u8],
    ) -> Res<Thread>
    where
        String: From<N>,
        String: From<A>,
        String: From<C>,
        String: From<B>,
    {
        /* file name */
        let f = mk_uuid();
        write_img(&f, i)?;

        let t = Thread {
            id: mk_uuid(),
            time: now_timestamp(),
            name: String::from(n),
            auth: String::from(a),
            cont: String::from(c),
            img: f,
        };

        db_log!("creating new thread: {t:?}");

        let b = String::from(b);
        let n = format!("{b}_{}", &t.id);
        sadd!(self, format!("{b}_threads"), &t.id)?;
        hset!(
            self,
            &n,
            &[
                ("id", &t.id),
                ("name", &t.name),
                ("auth", &t.auth),
                ("cont", &t.cont),
                ("img", &t.img),
            ]
        )?;
        /* set another type */
        hset!(self, &n, &[("time", t.time)])?;

        Ok(t)
    }

    pub fn new_post<B, T, A, C>(
        &mut self,
        b: B,
        t: T,
        a: A,
        c: C,
        i: Option<Vec<u8>>,
    ) -> Res<Post>
    where
        String: From<B>,
        String: From<T>,
        String: From<A>,
        String: From<C>,
    {
        let f = if let Some(i) = i {
            let f = mk_uuid();
            write_img(&f, &i)?;
            Some(f)
        } else {
            None
        };

        let t = String::from(t);
        let b = String::from(b);

        let p = Post {
            id: mk_uuid(),
            time: now_timestamp(),
            auth: String::from(a),
            cont: String::from(c),
            img: f,
        };
        let n = format!("{b}_{t}_{}", &p.id);

        sadd!(self, format!("child_{}", t), &p.id)?;
        hset!(
            self,
            &n,
            &[("id", &p.id), ("auth", &p.auth), ("cont", &p.cont)]
        )?;
        /* set another type */
        hset!(self, &n, &[("time", p.time)])?;
        /* set the image if it exists */
        if let Some(ref i) = p.img {
            hset!(self, &n, &[("img", i)])?;
        }

        Ok(p)
    }

    pub fn get_boards(&mut self) -> Res<HashSet<String>> {
        match self.con.smembers("boards") {
            Ok(x) => Ok(x),
            Err(e) => err_fmt!("error getting boards set: {e}"),
        }
    }

    pub fn get_posts<B, T>(&mut self, b: B, t: T) -> Res<Vec<Post>>
    where
        String: From<B>,
        String: From<T>,
    {
        let b = String::from(b);
        let t = String::from(t);

        let m: HashSet<String> = smembers!(self, format!("child_{t}"))?;
        let mut v = Vec::new();

        for x in m.iter() {
            let n = format!("{b}_{t}_{x}");
            db_log!("n: {n}");
            v.push(Post {
                id: (&*x).to_string(),
                time: hget!(self, &n, "time")?,
                auth: hget!(self, &n, "auth")?,
                cont: hget!(self, &n, "cont")?,
                img: match self.con.hget(&n, "img") {
                    Ok(x) => Some(x),
                    Err(_) => None,
                },
            });
        }

        db_log!("posts: {v:?}");

        v.sort_by_key(|x| x.time);
        Ok(v)
    }

    pub fn get_threads<T>(&mut self, b: T) -> Res<Vec<Thread>>
    where
        String: From<T>,
    {
        let b = String::from(b);
        let m: HashSet<String> = smembers!(self, format!("{b}_threads"))?;
        let mut v = Vec::new();

        for x in m.iter() {
            let n = format!("{b}_{x}");
            v.push(Thread {
                id: (&*x).to_string(),
                time: hget!(self, &n, "time")?,
                name: hget!(self, &n, "name")?,
                auth: hget!(self, &n, "auth")?,
                cont: hget!(self, &n, "cont")?,
                img: hget!(self, &n, "img")?,
            });
        }

        v.sort_by_key(|x| x.time);
        Ok(v)
    }

    pub fn get_thread<B, I>(&mut self, b: B, i: I) -> Res<Thread>
    where
        String: From<B>,
        String: From<I>,
    {
        let b = String::from(b);
        let i = String::from(i);
        let v = self.get_threads(&b)?;
        match v.iter().position(|x| &x.id == &i) {
            Some(x) => Ok(v[x].clone()),
            None => err_fmt!("no thread with id {i} found in /{b}/"),
        }
    }
}

pub fn mk_uuid() -> String {
    Uuid::new_v7(Timestamp::now(ContextV7::new())).to_string()
}