595 řádky
16 KiB
Rust
Spustitelný soubor
595 řádky
16 KiB
Rust
Spustitelný soubor
use chrono::Utc;
|
|
use redis::AsyncCommands;
|
|
use sha256::digest;
|
|
use sqlx::{query, query_as, types::Json};
|
|
use std::net::IpAddr;
|
|
|
|
use super::models::{Board, File, Post, Report};
|
|
use crate::{
|
|
ctx::Ctx,
|
|
error::NekrochanError,
|
|
live_hub::{PostCreatedMessage, PostRemovedMessage, PostUpdatedMessage},
|
|
GENERIC_PAGE_SIZE,
|
|
};
|
|
|
|
impl Post {
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn create(
|
|
ctx: &Ctx,
|
|
board: &Board,
|
|
thread: Option<i64>,
|
|
name: String,
|
|
tripcode: Option<String>,
|
|
capcode: Option<String>,
|
|
email: Option<String>,
|
|
content: String,
|
|
content_nomarkup: String,
|
|
files: Vec<File>,
|
|
password: String,
|
|
country: String,
|
|
ip: IpAddr,
|
|
bump: bool,
|
|
) -> Result<Self, NekrochanError> {
|
|
let post: Post = query_as(&format!(
|
|
r#"INSERT INTO posts_{}
|
|
(thread, name, tripcode, capcode, email, content, content_nomarkup, files, password, country, ip)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
RETURNING *"#, board.id)
|
|
)
|
|
.bind(thread)
|
|
.bind(name)
|
|
.bind(tripcode)
|
|
.bind(capcode)
|
|
.bind(email)
|
|
.bind(content)
|
|
.bind(content_nomarkup)
|
|
.bind(Json(files))
|
|
.bind(password)
|
|
.bind(country)
|
|
.bind(ip)
|
|
.fetch_one(ctx.db())
|
|
.await?;
|
|
|
|
if let Some(thread) = thread {
|
|
query(&format!(
|
|
"UPDATE posts_{} SET replies = replies + 1 WHERE id = $1",
|
|
board.id
|
|
))
|
|
.bind(thread)
|
|
.execute(ctx.db())
|
|
.await?;
|
|
|
|
if bump {
|
|
query(&format!(
|
|
"UPDATE posts_{} SET bumps = bumps + 1, bumped = CURRENT_TIMESTAMP WHERE id = $1",
|
|
board.id
|
|
))
|
|
.bind(thread)
|
|
.execute(ctx.db())
|
|
.await?;
|
|
}
|
|
} else {
|
|
delete_old_threads(ctx, board).await?;
|
|
|
|
ctx.cache().incr("total_threads", 1).await?;
|
|
ctx.cache()
|
|
.incr(format!("board_threads:{}", board.id), 1)
|
|
.await?;
|
|
}
|
|
|
|
let ip_key = format!("by_ip:{ip}");
|
|
let content_key = format!(
|
|
"by_content:{}",
|
|
digest(post.content_nomarkup.to_lowercase())
|
|
);
|
|
|
|
let member = format!("{}/{}", board.id, post.id);
|
|
let score = post.created.timestamp_micros();
|
|
|
|
ctx.cache().zadd(ip_key, &member, score).await?;
|
|
ctx.cache().zadd(content_key, &member, score).await?;
|
|
|
|
if thread.is_none() {
|
|
ctx.cache()
|
|
.set(format!("last_thread:{ip}"), post.created.timestamp_micros())
|
|
.await?;
|
|
}
|
|
|
|
Ok(post)
|
|
}
|
|
|
|
pub async fn create_report(
|
|
&self,
|
|
ctx: &Ctx,
|
|
reason: String,
|
|
reporter_country: String,
|
|
reporter_ip: IpAddr,
|
|
) -> Result<(), NekrochanError> {
|
|
let mut reports = self.reports.clone();
|
|
|
|
reports.push(Report {
|
|
reason,
|
|
reporter_country,
|
|
reporter_ip,
|
|
});
|
|
|
|
query(&format!(
|
|
"UPDATE posts_{} SET reported = CURRENT_TIMESTAMP, reports = $1 WHERE id = $2",
|
|
self.board
|
|
))
|
|
.bind(reports)
|
|
.bind(self.id)
|
|
.execute(ctx.db())
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn read(ctx: &Ctx, board: String, id: i64) -> Result<Option<Self>, NekrochanError> {
|
|
let post = query_as(&format!("SELECT * FROM posts_{} WHERE id = $1", board))
|
|
.bind(id)
|
|
.fetch_optional(ctx.db())
|
|
.await?;
|
|
|
|
Ok(post)
|
|
}
|
|
|
|
pub async fn read_board_page(
|
|
ctx: &Ctx,
|
|
board: &Board,
|
|
page: i64,
|
|
) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts = query_as(&format!(
|
|
r#"SELECT * FROM posts_{}
|
|
WHERE thread IS NULL
|
|
ORDER BY sticky DESC, bumped DESC
|
|
LIMIT $1
|
|
OFFSET $2"#,
|
|
board.id
|
|
))
|
|
.bind(board.config.0.page_size)
|
|
.bind((page - 1) * board.config.0.page_size)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn read_board_catalog(ctx: &Ctx, board: String) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts = query_as(&format!(
|
|
r#"SELECT * FROM posts_{board}
|
|
WHERE thread IS NULL
|
|
ORDER BY sticky DESC, bumped DESC"#
|
|
))
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn read_overboard_page(ctx: &Ctx, page: i64) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts = query_as(
|
|
r#"SELECT * FROM overboard
|
|
WHERE thread IS NULL
|
|
ORDER BY bumped DESC
|
|
LIMIT $1
|
|
OFFSET $2"#,
|
|
)
|
|
.bind(GENERIC_PAGE_SIZE)
|
|
.bind((page - 1) * GENERIC_PAGE_SIZE)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn read_overboard_catalog(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts = query_as(
|
|
r#"SELECT * FROM overboard
|
|
WHERE thread IS NULL
|
|
ORDER BY bumped DESC"#,
|
|
)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn read_reports_page(ctx: &Ctx, page: i64) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts = query_as(
|
|
r#"SELECT * FROM overboard
|
|
WHERE reports != '[]'::jsonb
|
|
ORDER BY jsonb_array_length(reports), reported DESC
|
|
LIMIT $1
|
|
OFFSET $2"#,
|
|
)
|
|
.bind(GENERIC_PAGE_SIZE)
|
|
.bind((page - 1) * GENERIC_PAGE_SIZE)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn read_ip_page(
|
|
ctx: &Ctx,
|
|
ip: IpAddr,
|
|
page: i64,
|
|
) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts = query_as(
|
|
r#"SELECT * FROM overboard
|
|
WHERE ip = $1
|
|
ORDER BY created DESC
|
|
LIMIT $2
|
|
OFFSET $3"#,
|
|
)
|
|
.bind(ip)
|
|
.bind(GENERIC_PAGE_SIZE)
|
|
.bind((page - 1) * GENERIC_PAGE_SIZE)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn read_replies(&self, ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
|
let replies = query_as(&format!(
|
|
"SELECT * FROM posts_{} WHERE thread = $1 ORDER BY sticky DESC, created ASC",
|
|
self.board
|
|
))
|
|
.bind(self.id)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(replies)
|
|
}
|
|
|
|
pub async fn read_replies_after(
|
|
&self,
|
|
ctx: &Ctx,
|
|
last: i64,
|
|
) -> Result<Vec<Self>, NekrochanError> {
|
|
let replies = query_as(&format!(
|
|
"SELECT * FROM posts_{} WHERE thread = $1 AND id > $2 ORDER BY created ASC",
|
|
self.board
|
|
))
|
|
.bind(self.id)
|
|
.bind(last)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(replies)
|
|
}
|
|
|
|
pub async fn read_all(ctx: &Ctx, board: String) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts = query_as(&format!("SELECT * FROM posts_{board}"))
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn read_all_overboard(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts = query_as("SELECT * FROM overboard")
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn read_by_query(
|
|
ctx: &Ctx,
|
|
board: &Board,
|
|
query: String,
|
|
page: i64,
|
|
) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts = query_as(&format!(
|
|
"SELECT * FROM posts_{} WHERE LOWER(content_nomarkup) LIKE LOWER($1) LIMIT $2 OFFSET $3",
|
|
board.id
|
|
))
|
|
.bind(format!("%{query}%"))
|
|
.bind(board.config.0.page_size)
|
|
.bind((page - 1) * board.config.0.page_size)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn read_by_query_overboard(
|
|
ctx: &Ctx,
|
|
query: String,
|
|
page: i64,
|
|
) -> Result<Vec<Self>, NekrochanError> {
|
|
let posts =
|
|
query_as("SELECT * FROM overboard WHERE LOWER(content_nomarkup) LIKE LOWER($1) LIMIT $2 OFFSET $3")
|
|
.bind(format!("%{query}%"))
|
|
.bind(GENERIC_PAGE_SIZE)
|
|
.bind((page - 1) * GENERIC_PAGE_SIZE)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
Ok(posts)
|
|
}
|
|
|
|
pub async fn update_user_id(&self, ctx: &Ctx, user_id: String) -> Result<(), NekrochanError> {
|
|
let post = query_as(&format!(
|
|
"UPDATE posts_{} SET user_id = $1 WHERE id = $2 RETURNING *",
|
|
self.board,
|
|
))
|
|
.bind(user_id)
|
|
.bind(self.id)
|
|
.fetch_one(ctx.db())
|
|
.await?;
|
|
|
|
ctx.hub().send(PostCreatedMessage { post }).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update_sticky(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
|
let post = query_as(&format!(
|
|
"UPDATE posts_{} SET sticky = NOT sticky WHERE id = $1 RETURNING *",
|
|
self.board
|
|
))
|
|
.bind(self.id)
|
|
.fetch_one(ctx.db())
|
|
.await?;
|
|
|
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update_lock(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
|
let post = query_as(&format!(
|
|
"UPDATE posts_{} SET locked = NOT locked WHERE id = $1 RETURNING *",
|
|
self.board
|
|
))
|
|
.bind(self.id)
|
|
.fetch_one(ctx.db())
|
|
.await?;
|
|
|
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update_content(
|
|
&self,
|
|
ctx: &Ctx,
|
|
content: String,
|
|
content_nomarkup: String,
|
|
) -> Result<(), NekrochanError> {
|
|
let post = query_as(&format!(
|
|
"UPDATE posts_{} SET content = $1, content_nomarkup = $2 WHERE id = $3 RETURNING *",
|
|
self.board
|
|
))
|
|
.bind(content)
|
|
.bind(&content_nomarkup)
|
|
.bind(self.id)
|
|
.fetch_optional(ctx.db())
|
|
.await?;
|
|
|
|
let Some(post) = post else { return Ok(()) };
|
|
|
|
let old_key = format!(
|
|
"by_content:{}",
|
|
digest(self.content_nomarkup.to_lowercase())
|
|
);
|
|
let new_key = format!("by_content:{}", digest(content_nomarkup.to_lowercase()));
|
|
let member = format!("{}/{}", self.board, self.id);
|
|
let score = Utc::now().timestamp_micros();
|
|
|
|
ctx.cache().zrem(old_key, &member).await?;
|
|
ctx.cache().zadd(new_key, &member, score).await?;
|
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update_quotes(&self, ctx: &Ctx, id: i64) -> Result<(), NekrochanError> {
|
|
let post = query_as(&format!(
|
|
"UPDATE posts_{} SET quotes = array_append(quotes, $1) WHERE id = $2 RETURNING *",
|
|
self.board
|
|
))
|
|
.bind(id)
|
|
.bind(self.id)
|
|
.fetch_one(ctx.db())
|
|
.await?;
|
|
|
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn update_spoiler(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
|
let mut files = self.files.clone();
|
|
|
|
for file in files.iter_mut() {
|
|
file.spoiler = !file.spoiler;
|
|
}
|
|
|
|
let post = query_as(&format!(
|
|
"UPDATE posts_{} SET files = $1 WHERE id = $2 RETURNING *",
|
|
self.board
|
|
))
|
|
.bind(Json(files))
|
|
.bind(self.id)
|
|
.fetch_one(ctx.db())
|
|
.await?;
|
|
|
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn delete(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
|
let to_be_deleted: Vec<Post> = query_as(&format!(
|
|
"SELECT * FROM posts_{} WHERE id = $1 OR thread = $1 ORDER BY id ASC",
|
|
self.board
|
|
))
|
|
.bind(self.id)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
for post in &to_be_deleted {
|
|
for file in post.files.iter() {
|
|
file.delete().await;
|
|
}
|
|
|
|
let id = post.id;
|
|
let url = post.post_url();
|
|
|
|
let live_quote = format!("<a class=\"quote\" href=\"{url}\">>>{id}</a>");
|
|
let dead_quote = format!("<span class=\"dead-quote\">>>{id}</span>");
|
|
|
|
let posts = query_as(&format!(
|
|
"UPDATE posts_{} SET content = REPLACE(content, $1, $2) WHERE content LIKE '%{}%' RETURNING *",
|
|
self.board, live_quote
|
|
))
|
|
.bind(live_quote)
|
|
.bind(dead_quote)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
for post in posts {
|
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
|
}
|
|
|
|
let posts = query_as(&format!(
|
|
"UPDATE posts_{} SET quotes = array_remove(quotes, $1) WHERE $1 = ANY(quotes) RETURNING *",
|
|
self.board
|
|
))
|
|
.bind(id)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
for post in posts {
|
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
|
}
|
|
|
|
let ip_key = format!("by_ip:{}", post.ip);
|
|
let content_key = format!(
|
|
"by_content:{}",
|
|
digest(post.content_nomarkup.to_lowercase())
|
|
);
|
|
|
|
let member = format!("{}/{}", post.board, post.id);
|
|
|
|
ctx.cache().zrem(ip_key, &member).await?;
|
|
ctx.cache().zrem(content_key, &member).await?;
|
|
ctx.hub()
|
|
.send(PostRemovedMessage { post: post.clone() })
|
|
.await?;
|
|
}
|
|
|
|
let in_list = to_be_deleted
|
|
.iter()
|
|
.map(|post| (post.id))
|
|
.collect::<Vec<_>>();
|
|
|
|
query(&format!(
|
|
"DELETE FROM posts_{} WHERE id = ANY($1)",
|
|
self.board
|
|
))
|
|
.bind(&in_list)
|
|
.execute(ctx.db())
|
|
.await?;
|
|
|
|
if let Some(thread) = self.thread {
|
|
query(&format!(
|
|
"UPDATE posts_{} SET replies = replies - 1 WHERE id = $1",
|
|
self.board
|
|
))
|
|
.bind(thread)
|
|
.execute(ctx.db())
|
|
.await?;
|
|
} else {
|
|
ctx.cache().decr("total_threads", 1).await?;
|
|
ctx.cache()
|
|
.decr(format!("board_threads:{}", self.board), 1)
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn delete_files(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
|
for file in &self.files.0 {
|
|
file.delete().await;
|
|
}
|
|
|
|
query(&format!(
|
|
"UPDATE posts_{} SET files = '[]'::jsonb WHERE id = $1",
|
|
self.board
|
|
))
|
|
.bind(self.id)
|
|
.execute(ctx.db())
|
|
.await?;
|
|
|
|
ctx.hub()
|
|
.send(PostUpdatedMessage { post: self.clone() })
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn delete_reports(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
|
query(&format!(
|
|
"UPDATE posts_{} SET reported = NULL, reports = '[]'::jsonb WHERE id = $1",
|
|
self.board
|
|
))
|
|
.bind(self.id)
|
|
.execute(ctx.db())
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Post {
|
|
pub fn post_url(&self) -> String {
|
|
format!(
|
|
"/boards/{}/{}#{}",
|
|
self.board,
|
|
self.thread.unwrap_or(self.id),
|
|
self.id
|
|
)
|
|
}
|
|
|
|
pub fn post_url_notarget(&self) -> String {
|
|
format!("/boards/{}/{}", self.board, self.id)
|
|
}
|
|
|
|
pub fn thread_url(&self) -> String {
|
|
format!("/boards/{}/{}", self.board, self.thread.unwrap_or(self.id),)
|
|
}
|
|
}
|
|
|
|
async fn delete_old_threads(ctx: &Ctx, board: &Board) -> Result<(), NekrochanError> {
|
|
let old_threads: Vec<Post> = query_as(&format!(
|
|
r#"SELECT * FROM posts_{}
|
|
WHERE thread IS NULL AND id NOT IN (
|
|
SELECT id
|
|
FROM (
|
|
SELECT id
|
|
FROM posts_{}
|
|
WHERE thread IS NULL
|
|
ORDER BY sticky DESC, bumped DESC
|
|
LIMIT $1
|
|
) catty
|
|
)"#,
|
|
board.id, board.id
|
|
))
|
|
.bind(board.config.0.page_size * board.config.0.page_count)
|
|
.fetch_all(ctx.db())
|
|
.await?;
|
|
|
|
for thread in &old_threads {
|
|
thread.delete(ctx).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|