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