251 řádky
7.3 KiB
Rust
Spustitelný soubor
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(())
|
|
}
|