Globální bannery

Tento commit je obsažen v:
sneedmaster 2023-12-16 13:51:50 +01:00
rodič 1eeffc71c6
revize bde9a95bd6
30 změnil soubory, kde provedl 172 přidání a 125 odebrání

Zobrazit soubor

@ -1 +0,0 @@
DROP TABLE accounts, boards, bans;

Zobrazit soubor

@ -0,0 +1,6 @@
ALTER TABLE boards DROP COLUMN banners;
CREATE TABLE banners (
id SERIAL NOT NULL PRIMARY KEY,
banner JSONB NOT NULL
);

Zobrazit soubor

@ -31,7 +31,6 @@ pub struct ServerCfg {
pub struct SiteCfg {
pub name: String,
pub description: String,
pub site_banner: Option<String>,
}
#[derive(Deserialize, Clone)]

64
src/db/banner.rs Normální soubor
Zobrazit soubor

@ -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<Self, NekrochanError> {
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<Option<Self>, NekrochanError> {
let banners: Vec<String> = 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<Vec<Self>, NekrochanError> {
let banners_str: Vec<String> = 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<Option<Self>, NekrochanError> {
let banner: Option<String> = 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(())
}
}

Zobrazit soubor

@ -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<File> {
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),

Zobrazit soubor

@ -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<Banner> = 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")

Zobrazit soubor

@ -3,5 +3,6 @@ pub mod models;
mod account;
mod ban;
mod banner;
mod board;
mod post;

Zobrazit soubor

@ -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<Vec<File>>,
pub config: Json<BoardCfg>,
pub created: DateTime<Utc>,
}
@ -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<File>,
}

Zobrazit soubor

@ -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;
}

Zobrazit soubor

@ -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 {

Zobrazit soubor

@ -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<NamedFile, NekrochanError> {
Ok(favicon)
}
#[post("/debug")]
async fn debug(req: String) -> HttpResponse {
println!("{req}");
#[get("/random-banner")]
async fn random_banner(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
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)]

Zobrazit soubor

@ -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)
}

Zobrazit soubor

@ -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<String>,
#[multipart(rename = "files[]")]
files: Vec<TempFile>,
}
@ -23,32 +22,20 @@ pub async fn add_banners(
) -> Result<HttpResponse, NekrochanError> {
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)

Zobrazit soubor

@ -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<usize>,
banners: Vec<i32>,
}
#[post("/staff/actions/remove-banners")]
@ -20,30 +20,18 @@ pub async fn remove_banners(
) -> Result<HttpResponse, NekrochanError> {
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)

Zobrazit soubor

@ -16,7 +16,7 @@ pub struct UpdatePermissionsForm {
staff_log: Option<String>,
reports: Option<String>,
bans: Option<String>,
board_banners: Option<String>,
banners: Option<String>,
board_config: Option<String>,
bypass_bans: Option<String>,
bypass_board_lock: Option<String>,
@ -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;
}

Zobrazit soubor

@ -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<Banner>,
}
#[get("/staff/banners/{board}")]
pub async fn banners(
ctx: Data<Ctx>,
req: HttpRequest,
board: Path<String>,
) -> Result<HttpResponse, NekrochanError> {
#[get("/staff/banners")]
pub async fn banners(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
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)
}

Zobrazit soubor

@ -24,10 +24,7 @@ pub async fn boards(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, Ne
let tcx = TemplateCtx::new(&ctx, &req).await?;
let account = account_from_auth(&ctx, &req).await?;
if !(account.perms().owner()
|| account.perms().board_config()
|| account.perms().board_banners())
{
if !(account.perms().owner() || account.perms().board_config() || account.perms().banners()) {
return Err(NekrochanError::InsufficientPermissionError);
}

binární
static/banner.gif

Binární soubor nebyl zobrazen.

Před

Šířka:  |  Výška:  |  Velikost: 206 KiB

binární
static/default-banner.png Normální soubor

Binární soubor nebyl zobrazen.

Za

Šířka:  |  Výška:  |  Velikost: 19 KiB

Zobrazit soubor

@ -8,9 +8,7 @@
{% block content %}
<div class="container">
<div class="center">
{% if let Some(banner) = board.random_banner() %}
<img class="banner" src="{{ banner.file_url() }}">
{% endif %}
<img class="banner" src="/random-banner">
<h1 class="title">Katalog (<a href="/boards/{{ board.id }}">/{{ board.id }}/</a>)</h1>
<p class="description">{{ board.description }}</p>
<a href="/boards/{{ board.id }}">Index</a>

Zobrazit soubor

@ -10,9 +10,7 @@
{% block content %}
<div class="container">
<div class="center">
{% if let Some(banner) = board.random_banner() %}
<img class="banner" src="{{ banner.file_url() }}">
{% endif %}
<img class="banner" src="/random-banner">
<h1 class="title">/{{ board.id }}/ - {{ board.name }}</h1>
<p class="description">{{ board.description }}</p>
<a href="/boards/{{ board.id }}/catalog">Katalog</a>

Zobrazit soubor

@ -5,9 +5,6 @@
{% block content %}
<div class="container">
<div class="center">
{% if let Some(banner) = tcx.cfg.site.site_banner %}
<img class="banner" src="{{ banner }}">
{% endif %}
<h1 class="title">{{ tcx.cfg.site.name }}</h1>
<p class="description">{{ tcx.cfg.site.description }}</p>
</div>

Zobrazit soubor

@ -3,7 +3,7 @@
<a href="/staff/account">[Účet]</a>&#32;
<a href="/staff/accounts">[Účty]</a>&#32;
{% if perms.owner() || perms.board_config() || perms.board_banners() %}
{% if perms.owner() || perms.board_config() || perms.banners() %}
<a href="/staff/boards">[Nástěnky]</a>&#32;
{% endif %}
@ -11,6 +11,10 @@
<a href="/staff/bans">[Bany]</a>&#32;
{% endif %}
{% if perms.owner() || perms.banners() %}
<a href="/staff/banners">[Bannery]</a>&#32;
{% endif %}
{% if perms.owner() || perms.reports() %}
<a href="/staff/reports">[Hlášení]</a>&#32;
{% endif %}

Zobrazit soubor

@ -8,9 +8,7 @@
{% block content %}
<div class="container">
<div class="center">
{% if let Some(banner) = tcx.cfg.site.site_banner %}
<img class="banner" src="{{ banner }}">
{% endif %}
<img class="banner" src="/random-banner">
<h1 class="title">Katalog nadnástěnky</h1>
<p class="description">Nově naťuknutá vlákna ze všech nástěnek</p>
<a href="/overboard">Index</a>

Zobrazit soubor

@ -9,9 +9,7 @@
{% block content %}
<div class="container">
<div class="center">
{% if let Some(banner) = tcx.cfg.site.site_banner %}
<img class="banner" src="{{ banner }}">
{% endif %}
<img class="banner" src="/random-banner">
<h1 class="title">Index nadnástěnky</h1>
<p class="description">Nově naťuknutá vlákna ze všech nástěnek</p>
<a href="/overboard/catalog">Katalog</a>

Zobrazit soubor

@ -2,27 +2,25 @@
{% extends "base.html" %}
{% block title %}Bannery (/{{ board.id }}/){% endblock %}
{% block title %}Bannery{% endblock %}
{% block content %}
<h1 class="title">Bannery (/{{ board.id }}/)</h1>
<h1 class="title">Bannery</h1>
{% call staff_nav::staff_nav(tcx.perms) %}
<hr>
<h2>Bannery</h2>
<form method="post" action="/staff/actions/remove-banners">
<input name="board" type="hidden" value="{{ board.id }}">
<div class="table-wrap">
<table class="data-table">
<tr>
<th></th>
<th>Banner</th>
</tr>
{% for banner in board.banners.0 %}
{% for banner in banners %}
<tr>
<td><input name="banners[]" type="checkbox" value="{{ loop.index0 }}"></td>
<td><input name="banners[]" type="checkbox" value="{{ banner.id }}"></td>
<td>
<img class="banner" src="{{ banner.file_url() }}">
<img class="banner" src="{{ banner.banner.file_url() }}">
</td>
</tr>
{% endfor %}
@ -34,8 +32,6 @@
<hr>
<h2>Přidat bannery</h2>
<form method="post" enctype="multipart/form-data" action="/staff/actions/add-banners">
<input name="board" type="hidden" value="{{ board.id }}">
<table class="form-table">
<tr>
<td class="label">Bannery</td>

Zobrazit soubor

@ -18,7 +18,6 @@
<th>Jméno</th>
<th>Popis</th>
<th>Vytvořena</th>
<th>Bannery</th>
<th>Nastavení</th>
</tr>
{% for board in boards %}
@ -28,7 +27,6 @@
<td>{{ board.name }}</td>
<td>{{ board.description }}</td>
<td>{{ board.created|czech_datetime }}</td>
<td>{% if tcx.perms.owner() || tcx.perms.board_banners() %}<a href="/staff/banners/{{ board.id }}">[Zobrazit]</a>{% else %}-{% endif %}</td>
<td>{% if tcx.perms.owner() || tcx.perms.board_config() %}<a href="/staff/board-config/{{ board.id }}">[Zobrazit]</a>{% else %}-{% endif %}</td>
</tr>
{% endfor %}

Zobrazit soubor

@ -71,10 +71,10 @@
</tr>
<tr>
<td class="label">Bannery nástěnek</td>
<td class="label">Bannery</td>
<td>
<div class="input-wrapper">
<input name="board_banners" type="checkbox"{% if account.perms().board_banners() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
<input name="banners" type="checkbox"{% if account.perms().banners() %} checked="checked"{% endif %}{% if !tcx.perms.owner() %} disabled=""{% endif %}>
</div>
</td>
</tr>

Zobrazit soubor

@ -9,9 +9,7 @@
{% block content %}
<div class="container">
<div class="center">
{% if let Some(banner) = board.random_banner() %}
<img class="banner" src="{{ banner.file_url() }}">
{% endif %}
<img class="banner" src="/random-banner">
<h1 class="title">/{{ board.id }}/ - {{ board.name }}</h1>
<p class="description">{{ board.description }}</p>
<a href="/boards/{{ board.id }}/catalog">Katalog</a>