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 { 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, } pub struct Db { pub con: Connection, } impl Db { pub fn new() -> Res { 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( &mut self, b: B, n: N, a: A, c: C, i: &[u8], ) -> Res where String: From, String: From, String: From, String: From, { /* 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( &mut self, b: B, t: T, a: A, c: C, i: Option>, ) -> Res where String: From, String: From, String: From, String: From, { 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> { match self.con.smembers("boards") { Ok(x) => Ok(x), Err(e) => err_fmt!("error getting boards set: {e}"), } } pub fn get_posts(&mut self, b: B, t: T) -> Res> where String: From, String: From, { let b = String::from(b); let t = String::from(t); let m: HashSet = 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(&mut self, b: T) -> Res> where String: From, { let b = String::from(b); let m: HashSet = 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(&mut self, b: B, i: I) -> Res where String: From, String: From, { 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() }