nekrochan/src/db/board.rs
2024-07-24 13:08:08 +02:00

251 řádky
7.3 KiB
Rust
Spustitelný soubor

use redis::{cmd, AsyncCommands, Connection, JsonAsyncCommands, JsonCommands};
use sqlx::{query, query_as, types::Json};
use std::collections::HashMap;
use super::models::{Board, File};
use crate::{cfg::BoardCfg, ctx::Ctx, error::NekrochanError};
impl Board {
pub async fn create(
ctx: &Ctx,
id: String,
name: String,
description: String,
) -> Result<Self, NekrochanError> {
let config = Json(ctx.cfg.board_defaults.clone());
let board: Board = query_as("INSERT INTO boards (id, name, description, config) VALUES ($1, $2, $3, $4) RETURNING *")
.bind(id)
.bind(name)
.bind(description)
.bind(config)
.fetch_one(ctx.db())
.await?;
query(&format!(
r#"CREATE TABLE posts_{} (
id BIGSERIAL NOT NULL PRIMARY KEY,
board VARCHAR(16) NOT NULL DEFAULT '{}' REFERENCES boards(id),
thread BIGINT DEFAULT NULL REFERENCES posts_{}(id),
name VARCHAR(32) NOT NULL,
user_id VARCHAR(6) NOT NULL DEFAULT '000000',
tripcode VARCHAR(12) DEFAULT NULL,
capcode VARCHAR(32) DEFAULT NULL,
email VARCHAR(256) DEFAULT NULL,
content TEXT NOT NULL,
content_nomarkup TEXT NOT NULL,
files JSONB NOT NULL,
password VARCHAR(64) DEFAULT NULL,
country VARCHAR(2) NOT NULL,
region VARCHAR(64) NOT NULL,
ip INET NOT NULL,
bumps INT NOT NULL DEFAULT 0,
replies INT NOT NULL DEFAULT 0,
quotes BIGINT[] NOT NULL DEFAULT '{{}}',
sticky BOOLEAN NOT NULL DEFAULT false,
locked BOOLEAN NOT NULL DEFAULT false,
reported TIMESTAMPTZ DEFAULT NULL,
reports JSONB NOT NULL DEFAULT '[]'::jsonb,
bumped TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
)"#,
board.id, board.id, board.id
))
.execute(ctx.db())
.await?;
ctx.cache()
.set(format!("board_threads:{}", board.id), 0)
.await?;
ctx.cache().lpush("board_ids", &board.id).await?;
ctx.cache()
.json_set(format!("boards:{}", board.id), ".", &board)
.await?;
cmd("SORT")
.arg("board_ids")
.arg("ALPHA")
.arg("STORE")
.arg("board_ids")
.query_async(&mut ctx.cache())
.await?;
update_overboard(ctx, Self::read_all(ctx).await?).await?;
Ok(board)
}
pub async fn read(ctx: &Ctx, id: String) -> Result<Option<Self>, NekrochanError> {
let board: Option<String> = ctx.cache().json_get(format!("boards:{id}"), ".").await?;
let board = match board {
Some(json) => Some(serde_json::from_str(&json)?),
None => None,
};
Ok(board)
}
pub fn read_sync(cache: &mut Connection, id: String) -> Result<Option<Self>, NekrochanError> {
let board: Option<String> = cache.json_get(format!("boards:{id}"), ".")?;
let board = match board {
Some(json) => Some(serde_json::from_str(&json)?),
None => None,
};
Ok(board)
}
pub async fn read_all(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
let mut boards = Vec::new();
let ids: Vec<String> = ctx.cache().lrange("board_ids", 0, -1).await?;
for id in ids {
if let Some(board) = Self::read(ctx, id).await? {
boards.push(board);
}
}
Ok(boards)
}
pub async fn read_all_map(ctx: &Ctx) -> Result<HashMap<String, Self>, NekrochanError> {
let mut boards = HashMap::new();
let ids: Vec<String> = ctx.cache().lrange("board_ids", 0, -1).await?;
for id in ids {
if let Some(board) = Self::read(ctx, id.clone()).await? {
boards.insert(id, board);
}
}
Ok(boards)
}
pub async fn read_post_count(&self, ctx: &Ctx) -> Result<i64, NekrochanError> {
let (count,) = query_as(&format!("SELECT last_value FROM posts_{}_id_seq", self.id))
.fetch_one(ctx.db())
.await?;
Ok(count)
}
pub async fn update_name(&self, ctx: &Ctx, name: String) -> Result<(), NekrochanError> {
query("UPDATE boards SET name = $1 WHERE id = $2")
.bind(&name)
.bind(&self.id)
.execute(ctx.db())
.await?;
ctx.cache()
.json_set(format!("boards:{}", self.id), "name", &name)
.await?;
Ok(())
}
pub async fn update_description(
&self,
ctx: &Ctx,
description: String,
) -> Result<(), NekrochanError> {
query("UPDATE boards SET description = $1 WHERE id = $2")
.bind(&description)
.bind(&self.id)
.execute(ctx.db())
.await?;
ctx.cache()
.json_set(format!("boards:{}", self.id), "description", &description)
.await?;
Ok(())
}
pub async fn update_banners(
&self,
ctx: &Ctx,
banners: Vec<File>,
) -> Result<(), NekrochanError> {
query("UPDATE boards SET banners = $1 WHERE id = $2")
.bind(Json(&banners))
.bind(&self.id)
.execute(ctx.db())
.await?;
ctx.cache()
.json_set(format!("boards:{}", self.id), "banners", &banners)
.await?;
Ok(())
}
pub async fn update_config(&self, ctx: &Ctx, config: BoardCfg) -> Result<(), NekrochanError> {
query("UPDATE boards SET config = $1 WHERE id = $2")
.bind(Json(&config))
.bind(&self.id)
.execute(ctx.db())
.await?;
ctx.cache()
.json_set(format!("boards:{}", self.id), "config", &config)
.await?;
Ok(())
}
pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
let boards = Self::read_all(ctx)
.await?
.into_iter()
.filter(|board| board.id != self.id)
.collect();
update_overboard(ctx, boards).await?;
query("DELETE FROM bans WHERE board = $1")
.bind(&self.id)
.execute(ctx.db())
.await?;
query(&format!("DROP TABLE posts_{}", self.id))
.execute(ctx.db())
.await?;
query("DELETE FROM boards WHERE id = $1")
.bind(&self.id)
.execute(ctx.db())
.await?;
ctx.cache().del(format!("boards:{}", self.id)).await?;
ctx.cache().lrem("board_ids", 0, &self.id).await?;
Ok(())
}
}
async fn update_overboard(ctx: &Ctx, boards: Vec<Board>) -> Result<(), NekrochanError> {
query("DROP VIEW IF EXISTS overboard")
.execute(ctx.db())
.await?;
if boards.is_empty() {
return Ok(());
}
let unions = boards
.into_iter()
.map(|board| format!("SELECT * FROM posts_{}", board.id))
.collect::<Vec<_>>()
.join(" UNION ");
query(&format!("CREATE VIEW overboard AS {unions}"))
.execute(ctx.db())
.await?;
Ok(())
}