diff --git a/migrations/20230710121446_create_tables.down.sql b/migrations/20230710121446_create_tables.down.sql deleted file mode 100755 index efacdfc..0000000 --- a/migrations/20230710121446_create_tables.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE accounts, boards, bans; diff --git a/migrations/20230710121446_create_tables.up.sql b/migrations/20230710121446_create_tables.sql similarity index 100% rename from migrations/20230710121446_create_tables.up.sql rename to migrations/20230710121446_create_tables.sql diff --git a/migrations/20231216092451_global_banners.sql b/migrations/20231216092451_global_banners.sql new file mode 100644 index 0000000..14888f3 --- /dev/null +++ b/migrations/20231216092451_global_banners.sql @@ -0,0 +1,6 @@ +ALTER TABLE boards DROP COLUMN banners; + +CREATE TABLE banners ( + id SERIAL NOT NULL PRIMARY KEY, + banner JSONB NOT NULL +); diff --git a/src/cfg.rs b/src/cfg.rs index e420f30..2947c06 100755 --- a/src/cfg.rs +++ b/src/cfg.rs @@ -31,7 +31,6 @@ pub struct ServerCfg { pub struct SiteCfg { pub name: String, pub description: String, - pub site_banner: Option, } #[derive(Deserialize, Clone)] diff --git a/src/db/banner.rs b/src/db/banner.rs new file mode 100644 index 0000000..ab2b68f --- /dev/null +++ b/src/db/banner.rs @@ -0,0 +1,64 @@ +use redis::AsyncCommands; +use sqlx::{query, query_as, types::Json}; + +use super::models::{Banner, File}; +use crate::{ctx::Ctx, NekrochanError}; + +impl Banner { + pub async fn create(ctx: &Ctx, banner: File) -> Result { + let banner: Self = query_as("INSERT INTO banners (banner) VALUES ($1) RETURNING *") + .bind(Json(banner)) + .fetch_one(ctx.db()) + .await?; + + ctx.cache() + .zadd("banners", serde_json::to_string(&banner)?, banner.id) + .await?; + + Ok(banner) + } + + pub async fn read(ctx: &Ctx, id: i32) -> Result, NekrochanError> { + let banners: Vec = ctx.cache().zrangebyscore("banners", id, id).await?; + let json = banners.get(0); + + let banner = match json { + Some(json) => Some(serde_json::from_str(json)?), + None => None, + }; + + Ok(banner) + } + + pub async fn read_all(ctx: &Ctx) -> Result, NekrochanError> { + let banners_str: Vec = ctx.cache().zrange("banners", 0, -1).await?; + let banners_json = format!("[{}]", banners_str.join(",")); // If it works, it works + let banners = serde_json::from_str(&banners_json)?; + + Ok(banners) + } + + pub async fn read_random(ctx: &Ctx) -> Result, NekrochanError> { + let banner: Option = ctx.cache().zrandmember("banners", None).await?; + + let banner = match banner { + Some(json) => Some(serde_json::from_str(&json)?), + None => None, + }; + + Ok(banner) + } + + pub async fn remove(&self, ctx: &Ctx) -> Result<(), NekrochanError> { + self.banner.delete().await; + + query("DELETE FROM banners WHERE id = $1") + .bind(self.id) + .execute(ctx.db()) + .await?; + + ctx.cache().zrembyscore("banners", self.id, self.id).await?; + + Ok(()) + } +} diff --git a/src/db/board.rs b/src/db/board.rs index 86dac43..1be3bf6 100755 --- a/src/db/board.rs +++ b/src/db/board.rs @@ -1,5 +1,4 @@ use captcha::{gen, Difficulty}; -use rand::{seq::SliceRandom, thread_rng}; use redis::{cmd, AsyncCommands, JsonAsyncCommands}; use sha256::digest; use sqlx::{query, query_as, types::Json}; @@ -207,12 +206,6 @@ impl Board { } impl Board { - pub fn random_banner(&self) -> Option { - self.banners - .choose(&mut thread_rng()) - .map(std::clone::Clone::clone) - } - pub fn thread_captcha(&self) -> Option<(String, String)> { let captcha = match self.config.thread_captcha.as_str() { "easy" => gen(Difficulty::Easy), diff --git a/src/db/cache.rs b/src/db/cache.rs index 1500f06..bf1a332 100644 --- a/src/db/cache.rs +++ b/src/db/cache.rs @@ -3,7 +3,7 @@ use redis::{cmd, AsyncCommands, JsonAsyncCommands}; use sha256::digest; use sqlx::query_as; -use super::models::{Account, Board, Post}; +use super::models::{Account, Banner, Board, Post}; use crate::ctx::Ctx; pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> { @@ -29,6 +29,16 @@ pub async fn init_cache(ctx: &Ctx) -> Result<(), Error> { ctx.cache().lpush("board_ids", &board.id).await?; } + let banners: Vec = query_as("SELECT * FROM banners") + .fetch_all(ctx.db()) + .await?; + + for banner in &banners { + ctx.cache() + .zadd("banners", serde_json::to_string(banner)?, banner.id) + .await?; + } + cmd("SORT") .arg("board_ids") .arg("ALPHA") diff --git a/src/db/mod.rs b/src/db/mod.rs index 0cdd13a..fd1cf01 100755 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -3,5 +3,6 @@ pub mod models; mod account; mod ban; +mod banner; mod board; mod post; diff --git a/src/db/models.rs b/src/db/models.rs index 20664da..7690140 100755 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -7,7 +7,7 @@ use sqlx::{types::Json, FromRow}; use crate::cfg::BoardCfg; -#[derive(FromRow, Clone, Serialize, Deserialize)] +#[derive(FromRow, Serialize, Deserialize, Clone)] pub struct Account { pub username: String, pub password: String, @@ -21,7 +21,6 @@ pub struct Board { pub id: String, pub name: String, pub description: String, - pub banners: Json>, pub config: Json, pub created: DateTime, } @@ -90,3 +89,9 @@ pub struct File { pub timestamp: i64, pub size: usize, } + +#[derive(FromRow, Serialize, Deserialize)] +pub struct Banner { + pub id: i32, + pub banner: Json, +} diff --git a/src/db/post.rs b/src/db/post.rs index a521b0c..2dd0b85 100755 --- a/src/db/post.rs +++ b/src/db/post.rs @@ -333,6 +333,10 @@ impl Post { .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(); @@ -381,7 +385,7 @@ impl Post { } pub async fn delete_files(&self, ctx: &Ctx) -> Result<(), NekrochanError> { - for file in self.files.iter() { + for file in &self.files.0 { file.delete().await; } diff --git a/src/files.rs b/src/files.rs index d7bbd9a..99633ab 100755 --- a/src/files.rs +++ b/src/files.rs @@ -11,7 +11,7 @@ use tokio::{ use crate::{ cfg::Cfg, ctx::Ctx, - db::models::{Board, File, Post}, + db::models::{Banner, Board, File, Post}, error::NekrochanError, }; @@ -310,13 +310,18 @@ pub async fn cleanup_files(ctx: &Ctx) -> Result<(), Error> { let mut keep = HashSet::new(); let mut keep_thumbs = HashSet::new(); + let banners = Banner::read_all(ctx).await?; + + for banner in banners { + keep.insert(format!( + "{}.{}", + banner.banner.timestamp, banner.banner.format + )); + } + let boards = Board::read_all(ctx).await?; for board in boards { - for file in board.banners.0 { - keep.insert(format!("{}.{}", file.timestamp, file.format)); - } - let posts = Post::read_all(ctx, board.id.clone()).await?; for post in posts { diff --git a/src/main.rs b/src/main.rs index 7b7693e..910d11e 100755 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,10 @@ use actix_web::{ body::MessageBody, dev::ServiceResponse, get, - http::StatusCode, + http::header::{HeaderValue, CACHE_CONTROL, PRAGMA}, middleware::{ErrorHandlerResponse, ErrorHandlers}, - post, web::Data, - App, HttpResponse, HttpServer, ResponseError, + App, HttpRequest, HttpResponse, HttpServer, ResponseError, }; use anyhow::Error; use askama::Template; @@ -15,7 +14,7 @@ use log::{error, info}; use nekrochan::{ cfg::Cfg, ctx::Ctx, - db::cache::init_cache, + db::{cache::init_cache, models::Banner}, error::NekrochanError, files::cleanup_files, web::{self, template_response}, @@ -98,8 +97,8 @@ async fn run() -> Result<(), Error> { .service(web::staff::actions::update_board_config::update_board_config) .service(web::staff::actions::update_boards::update_boards) .service(web::staff::actions::update_permissions::update_permissions) - .service(debug) .service(favicon) + .service(random_banner) .service(Files::new("/static", "./static")) .service(Files::new("/uploads", "./uploads").disable_content_disposition()) .wrap(ErrorHandlers::new().default_handler(error_handler)) @@ -118,11 +117,28 @@ async fn favicon() -> Result { Ok(favicon) } -#[post("/debug")] -async fn debug(req: String) -> HttpResponse { - println!("{req}"); +#[get("/random-banner")] +async fn random_banner(ctx: Data, req: HttpRequest) -> Result { + let file = if let Some(banner) = Banner::read_random(&ctx).await? { + let timestamp = banner.banner.timestamp; + let format = &banner.banner.format; - HttpResponse::new(StatusCode::OK) + NamedFile::open(format!("./uploads/{timestamp}.{format}"))? + } else { + NamedFile::open("./static/default-banner.png")? + }; + + let mut res = file.into_response(&req); + + res.headers_mut().append( + CACHE_CONTROL, + HeaderValue::from_static("no-cache, no-store, must-revalidate"), + ); + + res.headers_mut() + .append(PRAGMA, HeaderValue::from_static("no-cache")); + + Ok(res) } #[derive(Template)] diff --git a/src/perms.rs b/src/perms.rs index 28ccb24..8cbc61f 100755 --- a/src/perms.rs +++ b/src/perms.rs @@ -59,7 +59,7 @@ impl PermissionWrapper { self.0.contains(Permissions::Bans) } - pub fn board_banners(&self) -> bool { + pub fn banners(&self) -> bool { self.0.contains(Permissions::BoardBanners) } diff --git a/src/web/staff/actions/add_banners.rs b/src/web/staff/actions/add_banners.rs index 3c39652..5e97775 100755 --- a/src/web/staff/actions/add_banners.rs +++ b/src/web/staff/actions/add_banners.rs @@ -1,16 +1,15 @@ -use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; +use actix_multipart::form::{tempfile::TempFile, MultipartForm}; use actix_web::{post, web::Data, HttpRequest, HttpResponse}; use crate::{ ctx::Ctx, - db::models::{Board, File}, + db::models::{Banner, File}, error::NekrochanError, web::tcx::account_from_auth, }; #[derive(MultipartForm)] pub struct AddBannersForm { - board: Text, #[multipart(rename = "files[]")] files: Vec, } @@ -23,32 +22,20 @@ pub async fn add_banners( ) -> Result { let account = account_from_auth(&ctx, &req).await?; - if !(account.perms().owner() || account.perms().board_banners()) { + if !(account.perms().owner() || account.perms().banners()) { return Err(NekrochanError::InsufficientPermissionError); } - let board = form.board.0; - let board = Board::read(&ctx, board.clone()) - .await? - .ok_or(NekrochanError::BoardNotFound(board))?; - - let mut new_banners = board.banners.0.clone(); - let added_banners = form.files; - let mut cfg = ctx.cfg.clone(); cfg.files.videos = false; - for banner in added_banners { - let file = File::new(&cfg, banner, false, false).await?; - - new_banners.push(file); + for file in form.files { + Banner::create(&ctx, File::new(&cfg, file, false, false).await?).await?; } - board.update_banners(&ctx, new_banners).await?; - let res = HttpResponse::SeeOther() - .append_header(("Location", format!("/staff/banners/{}", board.id).as_str())) + .append_header(("Location", "/staff/banners")) .finish(); Ok(res) diff --git a/src/web/staff/actions/remove_banners.rs b/src/web/staff/actions/remove_banners.rs index b4481ef..9be373b 100755 --- a/src/web/staff/actions/remove_banners.rs +++ b/src/web/staff/actions/remove_banners.rs @@ -2,14 +2,14 @@ use actix_web::{post, web::Data, HttpRequest, HttpResponse}; use serde::Deserialize; use crate::{ - ctx::Ctx, db::models::Board, error::NekrochanError, qsform::QsForm, web::tcx::account_from_auth, + ctx::Ctx, db::models::Banner, error::NekrochanError, qsform::QsForm, + web::tcx::account_from_auth, }; #[derive(Deserialize)] pub struct RemoveBannersForm { - board: String, #[serde(default)] - banners: Vec, + banners: Vec, } #[post("/staff/actions/remove-banners")] @@ -20,30 +20,18 @@ pub async fn remove_banners( ) -> Result { let account = account_from_auth(&ctx, &req).await?; - if !(account.perms().owner() || account.perms().board_banners()) { + if !(account.perms().owner() || account.perms().banners()) { return Err(NekrochanError::InsufficientPermissionError); } - let board = form.board; - let board = Board::read(&ctx, board.clone()) - .await? - .ok_or(NekrochanError::BoardNotFound(board))?; - - let old_banners = board.banners.0.clone(); - let mut new_banners = Vec::new(); - - for (i, banner) in old_banners.into_iter().enumerate() { - if form.banners.contains(&i) { - banner.delete().await; - } else { - new_banners.push(banner); + for id in form.banners { + if let Some(banner) = Banner::read(&ctx, id).await? { + banner.remove(&ctx).await?; } } - board.update_banners(&ctx, new_banners).await?; - let res = HttpResponse::SeeOther() - .append_header(("Location", format!("/staff/banners/{}", board.id).as_str())) + .append_header(("Location", "/staff/banners")) .finish(); Ok(res) diff --git a/src/web/staff/actions/update_permissions.rs b/src/web/staff/actions/update_permissions.rs index c4765d8..39c05d8 100755 --- a/src/web/staff/actions/update_permissions.rs +++ b/src/web/staff/actions/update_permissions.rs @@ -16,7 +16,7 @@ pub struct UpdatePermissionsForm { staff_log: Option, reports: Option, bans: Option, - board_banners: Option, + banners: Option, board_config: Option, bypass_bans: Option, bypass_board_lock: Option, @@ -67,7 +67,7 @@ pub async fn update_permissions( permissions |= Permissions::Bans; } - if form.board_banners.is_some() { + if form.banners.is_some() { permissions |= Permissions::BoardBanners; } diff --git a/src/web/staff/banners.rs b/src/web/staff/banners.rs index 58f9e9b..621c42f 100755 --- a/src/web/staff/banners.rs +++ b/src/web/staff/banners.rs @@ -1,13 +1,9 @@ -use actix_web::{ - get, - web::{Data, Path}, - HttpRequest, HttpResponse, -}; +use actix_web::{get, web::Data, HttpRequest, HttpResponse}; use askama::Template; use crate::{ ctx::Ctx, - db::models::Board, + db::models::Banner, error::NekrochanError, web::{ tcx::{account_from_auth, TemplateCtx}, @@ -19,28 +15,20 @@ use crate::{ #[template(path = "staff/banners.html")] struct BannersTemplate { tcx: TemplateCtx, - board: Board, + banners: Vec, } -#[get("/staff/banners/{board}")] -pub async fn banners( - ctx: Data, - req: HttpRequest, - board: Path, -) -> Result { +#[get("/staff/banners")] +pub async fn banners(ctx: Data, req: HttpRequest) -> Result { let tcx = TemplateCtx::new(&ctx, &req).await?; let account = account_from_auth(&ctx, &req).await?; - if !(account.perms().owner() || account.perms().board_banners()) { + if !(account.perms().owner() || account.perms().banners()) { return Err(NekrochanError::InsufficientPermissionError); } - let board = board.into_inner(); - let board = Board::read(&ctx, board.clone()) - .await? - .ok_or(NekrochanError::BoardNotFound(board))?; - - let template = BannersTemplate { tcx, board }; + let banners = Banner::read_all(&ctx).await?; + let template = BannersTemplate { tcx, banners }; template_response(&template) } diff --git a/src/web/staff/boards.rs b/src/web/staff/boards.rs index cf4d93d..74017ab 100755 --- a/src/web/staff/boards.rs +++ b/src/web/staff/boards.rs @@ -24,10 +24,7 @@ pub async fn boards(ctx: Data, req: HttpRequest) -> Result
- {% if let Some(banner) = board.random_banner() %} - - {% endif %} +

Katalog (/{{ board.id }}/)

{{ board.description }}

Index diff --git a/templates/board.html b/templates/board.html index f2239dc..2267acd 100755 --- a/templates/board.html +++ b/templates/board.html @@ -10,9 +10,7 @@ {% block content %}
- {% if let Some(banner) = board.random_banner() %} - - {% endif %} +

/{{ board.id }}/ - {{ board.name }}

{{ board.description }}

Katalog diff --git a/templates/index.html b/templates/index.html index 3d13377..d355599 100755 --- a/templates/index.html +++ b/templates/index.html @@ -5,9 +5,6 @@ {% block content %}
- {% if let Some(banner) = tcx.cfg.site.site_banner %} - - {% endif %}

{{ tcx.cfg.site.name }}

{{ tcx.cfg.site.description }}

diff --git a/templates/macros/staff-nav.html b/templates/macros/staff-nav.html index 171dc61..f2503df 100644 --- a/templates/macros/staff-nav.html +++ b/templates/macros/staff-nav.html @@ -3,7 +3,7 @@ [Účet] [Účty] - {% if perms.owner() || perms.board_config() || perms.board_banners() %} + {% if perms.owner() || perms.board_config() || perms.banners() %} [Nástěnky] {% endif %} @@ -11,6 +11,10 @@ [Bany] {% endif %} + {% if perms.owner() || perms.banners() %} + [Bannery] + {% endif %} + {% if perms.owner() || perms.reports() %} [Hlášení] {% endif %} diff --git a/templates/overboard-catalog.html b/templates/overboard-catalog.html index 5dd507c..7da3418 100755 --- a/templates/overboard-catalog.html +++ b/templates/overboard-catalog.html @@ -8,9 +8,7 @@ {% block content %}
- {% if let Some(banner) = tcx.cfg.site.site_banner %} - - {% endif %} +

Katalog nadnástěnky

Nově naťuknutá vlákna ze všech nástěnek

Index diff --git a/templates/overboard.html b/templates/overboard.html index d35520d..2e90cbd 100644 --- a/templates/overboard.html +++ b/templates/overboard.html @@ -9,9 +9,7 @@ {% block content %}
- {% if let Some(banner) = tcx.cfg.site.site_banner %} - - {% endif %} +

Index nadnástěnky

Nově naťuknutá vlákna ze všech nástěnek

Katalog diff --git a/templates/staff/banners.html b/templates/staff/banners.html index ce00b12..e2ac3ee 100755 --- a/templates/staff/banners.html +++ b/templates/staff/banners.html @@ -2,27 +2,25 @@ {% extends "base.html" %} -{% block title %}Bannery (/{{ board.id }}/){% endblock %} +{% block title %}Bannery{% endblock %} {% block content %} -

Bannery (/{{ board.id }}/)

+

Bannery

{% call staff_nav::staff_nav(tcx.perms) %}

Bannery

- -
- {% for banner in board.banners.0 %} + {% for banner in banners %} - + {% endfor %} @@ -34,8 +32,6 @@

Přidat bannery

- -
Banner
- +
diff --git a/templates/staff/boards.html b/templates/staff/boards.html index 26e9b48..53be523 100755 --- a/templates/staff/boards.html +++ b/templates/staff/boards.html @@ -18,7 +18,6 @@ - {% for board in boards %} @@ -28,7 +27,6 @@ - {% endfor %} diff --git a/templates/staff/permissions.html b/templates/staff/permissions.html index 4cfce88..ff45d70 100755 --- a/templates/staff/permissions.html +++ b/templates/staff/permissions.html @@ -71,10 +71,10 @@ - + diff --git a/templates/thread.html b/templates/thread.html index a40a1f6..ff1cdad 100644 --- a/templates/thread.html +++ b/templates/thread.html @@ -9,9 +9,7 @@ {% block content %}
- {% if let Some(banner) = board.random_banner() %} - - {% endif %} +

/{{ board.id }}/ - {{ board.name }}

{{ board.description }}

Katalog
BanneryJméno Popis VytvořenaBannery Nastavení
{{ board.name }} {{ board.description }} {{ board.created|czech_datetime }}{% if tcx.perms.owner() || tcx.perms.board_banners() %}[Zobrazit]{% else %}-{% endif %} {% if tcx.perms.owner() || tcx.perms.board_config() %}[Zobrazit]{% else %}-{% endif %}
Bannery nástěnekBannery
- +